etassia 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +140 -0
- data/LICENSE +25 -0
- data/README.adoc +436 -0
- data/exe/etassia +7 -0
- data/lib/etassia/cli.rb +187 -0
- data/lib/etassia/commands/batch.rb +210 -0
- data/lib/etassia/commands/notation_loader.rb +22 -0
- data/lib/etassia/commands/render.rb +98 -0
- data/lib/etassia/commands/types.rb +33 -0
- data/lib/etassia/commands/version.rb +24 -0
- data/lib/etassia/corpus_type_names.rb +52 -0
- data/lib/etassia/diagram/architecture.rb +108 -0
- data/lib/etassia/diagram/base.rb +61 -0
- data/lib/etassia/diagram/block.rb +102 -0
- data/lib/etassia/diagram/c4.rb +337 -0
- data/lib/etassia/diagram/class_diagram.rb +466 -0
- data/lib/etassia/diagram/class_namespace.rb +17 -0
- data/lib/etassia/diagram/class_note.rb +17 -0
- data/lib/etassia/diagram/containment.rb +107 -0
- data/lib/etassia/diagram/er_diagram.rb +317 -0
- data/lib/etassia/diagram/error.rb +38 -0
- data/lib/etassia/diagram/flowchart.rb +419 -0
- data/lib/etassia/diagram/flowchart_label_text.rb +63 -0
- data/lib/etassia/diagram/gantt.rb +94 -0
- data/lib/etassia/diagram/generic_text.rb +30 -0
- data/lib/etassia/diagram/git_graph.rb +57 -0
- data/lib/etassia/diagram/info.rb +38 -0
- data/lib/etassia/diagram/kanban.rb +198 -0
- data/lib/etassia/diagram/mindmap.rb +71 -0
- data/lib/etassia/diagram/packet.rb +93 -0
- data/lib/etassia/diagram/pie.rb +115 -0
- data/lib/etassia/diagram/quadrant.rb +135 -0
- data/lib/etassia/diagram/radar.rb +65 -0
- data/lib/etassia/diagram/requirement.rb +168 -0
- data/lib/etassia/diagram/sankey.rb +217 -0
- data/lib/etassia/diagram/sequence.rb +299 -0
- data/lib/etassia/diagram/sequence_box.rb +18 -0
- data/lib/etassia/diagram/sequence_frame.rb +37 -0
- data/lib/etassia/diagram/sequence_frame_section.rb +23 -0
- data/lib/etassia/diagram/sequence_text.rb +69 -0
- data/lib/etassia/diagram/sequence_wrap.rb +23 -0
- data/lib/etassia/diagram/state_diagram.rb +282 -0
- data/lib/etassia/diagram/timeline.rb +183 -0
- data/lib/etassia/diagram/treemap.rb +95 -0
- data/lib/etassia/diagram/user_journey.rb +153 -0
- data/lib/etassia/diagram/xy_chart.rb +90 -0
- data/lib/etassia/diagram.rb +8 -0
- data/lib/etassia/diagram_registry.rb +34 -0
- data/lib/etassia/engine.rb +334 -0
- data/lib/etassia/error/diagram_type_error.rb +10 -0
- data/lib/etassia/error/layout_error.rb +12 -0
- data/lib/etassia/error/notation_registration_error.rb +10 -0
- data/lib/etassia/error/parse_error.rb +11 -0
- data/lib/etassia/error/pipeline_error.rb +10 -0
- data/lib/etassia/error/render_error.rb +12 -0
- data/lib/etassia/error/score_error.rb +12 -0
- data/lib/etassia/error.rb +10 -0
- data/lib/etassia/error_report.rb +69 -0
- data/lib/etassia/ir/data.rb +39 -0
- data/lib/etassia/ir/data_value.rb +35 -0
- data/lib/etassia/ir/dimension.rb +10 -0
- data/lib/etassia/ir/edge.rb +34 -0
- data/lib/etassia/ir/graph.rb +28 -0
- data/lib/etassia/ir/integrity.rb +80 -0
- data/lib/etassia/ir/item.rb +46 -0
- data/lib/etassia/ir/model.rb +56 -0
- data/lib/etassia/ir/node.rb +10 -0
- data/lib/etassia/ir/placement.rb +47 -0
- data/lib/etassia/ir/prepositioned.rb +25 -0
- data/lib/etassia/ir/prepositioned_item.rb +26 -0
- data/lib/etassia/ir/property_set.rb +43 -0
- data/lib/etassia/ir/scalar.rb +25 -0
- data/lib/etassia/ir/series.rb +10 -0
- data/lib/etassia/ir.rb +17 -0
- data/lib/etassia/js_math.rb +511 -0
- data/lib/etassia/js_number.rb +64 -0
- data/lib/etassia/layout/architecture.rb +1472 -0
- data/lib/etassia/layout/base.rb +276 -0
- data/lib/etassia/layout/block.rb +738 -0
- data/lib/etassia/layout/c4.rb +876 -0
- data/lib/etassia/layout/c4_boundary_metrics.rb +35 -0
- data/lib/etassia/layout/c4_bounds.rb +106 -0
- data/lib/etassia/layout/c4_intersection.rb +77 -0
- data/lib/etassia/layout/c4_placement.rb +135 -0
- data/lib/etassia/layout/c4_shape_metrics.rb +97 -0
- data/lib/etassia/layout/c4_shape_outline.rb +67 -0
- data/lib/etassia/layout/c4_stereotype.rb +30 -0
- data/lib/etassia/layout/c4_text.rb +54 -0
- data/lib/etassia/layout/class_cell_placement.rb +148 -0
- data/lib/etassia/layout/class_diagram.rb +1116 -0
- data/lib/etassia/layout/class_diagram_namespaces.rb +26 -0
- data/lib/etassia/layout/class_diagram_notes.rb +26 -0
- data/lib/etassia/layout/class_name_advances.rb +31 -0
- data/lib/etassia/layout/class_namespace_box.rb +21 -0
- data/lib/etassia/layout/class_namespace_nodes.rb +197 -0
- data/lib/etassia/layout/class_namespace_placement.rb +71 -0
- data/lib/etassia/layout/class_note.rb +44 -0
- data/lib/etassia/layout/class_note_nodes.rb +84 -0
- data/lib/etassia/layout/elk_notices.rb +48 -0
- data/lib/etassia/layout/elk_placement.rb +197 -0
- data/lib/etassia/layout/er_diagram.rb +1002 -0
- data/lib/etassia/layout/er_diagram_adjacency.rb +20 -0
- data/lib/etassia/layout/er_diagram_alignment.rb +72 -0
- data/lib/etassia/layout/er_diagram_compaction.rb +85 -0
- data/lib/etassia/layout/er_diagram_conflicts.rb +70 -0
- data/lib/etassia/layout/er_diagram_coordinates.rb +104 -0
- data/lib/etassia/layout/er_diagram_entity_size.rb +82 -0
- data/lib/etassia/layout/er_diagram_layering.rb +109 -0
- data/lib/etassia/layout/er_diagram_ordering.rb +143 -0
- data/lib/etassia/layout/er_diagram_placement.rb +163 -0
- data/lib/etassia/layout/er_diagram_placement_node.rb +22 -0
- data/lib/etassia/layout/error.rb +133 -0
- data/lib/etassia/layout/flowchart/brandes_koepf.rb +208 -0
- data/lib/etassia/layout/flowchart/cluster_reach.rb +70 -0
- data/lib/etassia/layout/flowchart/component_packing.rb +129 -0
- data/lib/etassia/layout/flowchart/directed_placement.rb +428 -0
- data/lib/etassia/layout/flowchart/edge_chains.rb +111 -0
- data/lib/etassia/layout/flowchart/edge_order.rb +34 -0
- data/lib/etassia/layout/flowchart/edge_router.rb +533 -0
- data/lib/etassia/layout/flowchart/label_backdrop_geometry.rb +62 -0
- data/lib/etassia/layout/flowchart/layer_spacing.rb +85 -0
- data/lib/etassia/layout/flowchart/level_edges.rb +74 -0
- data/lib/etassia/layout/flowchart/level_frame.rb +47 -0
- data/lib/etassia/layout/flowchart/minimum_rank_spacing.rb +74 -0
- data/lib/etassia/layout/flowchart/row_centering.rb +229 -0
- data/lib/etassia/layout/flowchart.rb +1290 -0
- data/lib/etassia/layout/gantt.rb +814 -0
- data/lib/etassia/layout/gantt_duration.rb +49 -0
- data/lib/etassia/layout/gantt_exclusions.rb +74 -0
- data/lib/etassia/layout/gantt_ticks.rb +165 -0
- data/lib/etassia/layout/git_graph.rb +1075 -0
- data/lib/etassia/layout/grid.rb +165 -0
- data/lib/etassia/layout/info.rb +113 -0
- data/lib/etassia/layout/kanban.rb +452 -0
- data/lib/etassia/layout/kanban_card_text.rb +95 -0
- data/lib/etassia/layout/mindmap.rb +425 -0
- data/lib/etassia/layout/mindmap_cose.rb +81 -0
- data/lib/etassia/layout/mindmap_cose_body.rb +93 -0
- data/lib/etassia/layout/mindmap_cose_geometry.rb +190 -0
- data/lib/etassia/layout/mindmap_cose_grid.rb +96 -0
- data/lib/etassia/layout/mindmap_cose_link.rb +42 -0
- data/lib/etassia/layout/mindmap_cose_relaxation.rb +166 -0
- data/lib/etassia/layout/mindmap_cose_seed.rb +93 -0
- data/lib/etassia/layout/mindmap_cose_wedge.rb +37 -0
- data/lib/etassia/layout/mindmap_node_size.rb +130 -0
- data/lib/etassia/layout/packet.rb +285 -0
- data/lib/etassia/layout/pie.rb +305 -0
- data/lib/etassia/layout/pie_legend.rb +64 -0
- data/lib/etassia/layout/pie_legend_entry.rb +21 -0
- data/lib/etassia/layout/quadrant.rb +402 -0
- data/lib/etassia/layout/radar.rb +442 -0
- data/lib/etassia/layout/requirement.rb +765 -0
- data/lib/etassia/layout/sankey.rb +501 -0
- data/lib/etassia/layout/scene.rb +18 -0
- data/lib/etassia/layout/sequence/actor_gaps.rb +151 -0
- data/lib/etassia/layout/sequence/box_shape.rb +23 -0
- data/lib/etassia/layout/sequence/creations.rb +73 -0
- data/lib/etassia/layout/sequence/frame_divider.rb +22 -0
- data/lib/etassia/layout/sequence/frame_placement.rb +424 -0
- data/lib/etassia/layout/sequence/frame_reader.rb +75 -0
- data/lib/etassia/layout/sequence/frame_shape.rb +45 -0
- data/lib/etassia/layout/sequence/geometry.rb +24 -0
- data/lib/etassia/layout/sequence/message_rows.rb +165 -0
- data/lib/etassia/layout/sequence/note.rb +16 -0
- data/lib/etassia/layout/sequence/note_box.rb +114 -0
- data/lib/etassia/layout/sequence/note_placement.rb +223 -0
- data/lib/etassia/layout/sequence/text_width.rb +69 -0
- data/lib/etassia/layout/sequence/text_wrap.rb +93 -0
- data/lib/etassia/layout/sequence.rb +936 -0
- data/lib/etassia/layout/state_diagram.rb +1277 -0
- data/lib/etassia/layout/timeline.rb +165 -0
- data/lib/etassia/layout/timeline_card.rb +142 -0
- data/lib/etassia/layout/timeline_flow.rb +164 -0
- data/lib/etassia/layout/timeline_line.rb +33 -0
- data/lib/etassia/layout/timeline_rounding.rb +19 -0
- data/lib/etassia/layout/timeline_text.rb +105 -0
- data/lib/etassia/layout/treemap.rb +303 -0
- data/lib/etassia/layout/treemap_placement.rb +52 -0
- data/lib/etassia/layout/treemap_squarify.rb +108 -0
- data/lib/etassia/layout/user_journey.rb +300 -0
- data/lib/etassia/layout/user_journey_face.rb +33 -0
- data/lib/etassia/layout/user_journey_face_geometry.rb +80 -0
- data/lib/etassia/layout/user_journey_geometry.rb +336 -0
- data/lib/etassia/layout/user_journey_legend.rb +84 -0
- data/lib/etassia/layout/xy_chart.rb +620 -0
- data/lib/etassia/layout.rb +19 -0
- data/lib/etassia/lint_debt_scoreboard.rb +219 -0
- data/lib/etassia/markdown_text/emphasis_simulator.rb +373 -0
- data/lib/etassia/markdown_text.rb +393 -0
- data/lib/etassia/notation/builtin.rb +39 -0
- data/lib/etassia/notation/entry.rb +15 -0
- data/lib/etassia/notation/mermaid/class_diagram_config.rb +42 -0
- data/lib/etassia/notation/mermaid/git_graph_config.rb +46 -0
- data/lib/etassia/notation/mermaid/ir_adapter.rb +27 -0
- data/lib/etassia/notation/mermaid/ir_adapters/architecture.rb +196 -0
- data/lib/etassia/notation/mermaid/ir_adapters/block.rb +138 -0
- data/lib/etassia/notation/mermaid/ir_adapters/c4.rb +258 -0
- data/lib/etassia/notation/mermaid/ir_adapters/class_diagram.rb +257 -0
- data/lib/etassia/notation/mermaid/ir_adapters/er_diagram.rb +220 -0
- data/lib/etassia/notation/mermaid/ir_adapters/error.rb +32 -0
- data/lib/etassia/notation/mermaid/ir_adapters/flowchart.rb +208 -0
- data/lib/etassia/notation/mermaid/ir_adapters/gantt.rb +236 -0
- data/lib/etassia/notation/mermaid/ir_adapters/git_graph.rb +186 -0
- data/lib/etassia/notation/mermaid/ir_adapters/info.rb +34 -0
- data/lib/etassia/notation/mermaid/ir_adapters/kanban.rb +110 -0
- data/lib/etassia/notation/mermaid/ir_adapters/mindmap.rb +76 -0
- data/lib/etassia/notation/mermaid/ir_adapters/packet.rb +51 -0
- data/lib/etassia/notation/mermaid/ir_adapters/pie.rb +71 -0
- data/lib/etassia/notation/mermaid/ir_adapters/quadrant.rb +116 -0
- data/lib/etassia/notation/mermaid/ir_adapters/radar.rb +142 -0
- data/lib/etassia/notation/mermaid/ir_adapters/requirement.rb +237 -0
- data/lib/etassia/notation/mermaid/ir_adapters/sankey.rb +74 -0
- data/lib/etassia/notation/mermaid/ir_adapters/sequence.rb +304 -0
- data/lib/etassia/notation/mermaid/ir_adapters/state_diagram.rb +225 -0
- data/lib/etassia/notation/mermaid/ir_adapters/timeline.rb +93 -0
- data/lib/etassia/notation/mermaid/ir_adapters/treemap.rb +143 -0
- data/lib/etassia/notation/mermaid/ir_adapters/user_journey.rb +127 -0
- data/lib/etassia/notation/mermaid/ir_adapters/xychart.rb +131 -0
- data/lib/etassia/notation/mermaid.rb +399 -0
- data/lib/etassia/notation/parsed.rb +31 -0
- data/lib/etassia/notation/plantuml/activity/arrow_head.rb +45 -0
- data/lib/etassia/notation/plantuml/activity/arrow_style.rb +53 -0
- data/lib/etassia/notation/plantuml/activity/bar_columns.rb +75 -0
- data/lib/etassia/notation/plantuml/activity/box.rb +60 -0
- data/lib/etassia/notation/plantuml/activity/builder.rb +77 -0
- data/lib/etassia/notation/plantuml/activity/canvas.rb +117 -0
- data/lib/etassia/notation/plantuml/activity/column_centres.rb +151 -0
- data/lib/etassia/notation/plantuml/activity/columns.rb +224 -0
- data/lib/etassia/notation/plantuml/activity/diagram.rb +35 -0
- data/lib/etassia/notation/plantuml/activity/edge.rb +35 -0
- data/lib/etassia/notation/plantuml/activity/edge_marks.rb +68 -0
- data/lib/etassia/notation/plantuml/activity/exits.rb +61 -0
- data/lib/etassia/notation/plantuml/activity/frame_marks.rb +116 -0
- data/lib/etassia/notation/plantuml/activity/lane_steps.rb +82 -0
- data/lib/etassia/notation/plantuml/activity/lane_titles.rb +46 -0
- data/lib/etassia/notation/plantuml/activity/layout.rb +50 -0
- data/lib/etassia/notation/plantuml/activity/legacy_parser.rb +181 -0
- data/lib/etassia/notation/plantuml/activity/legacy_row_gaps.rb +65 -0
- data/lib/etassia/notation/plantuml/activity/mark.rb +23 -0
- data/lib/etassia/notation/plantuml/activity/marks.rb +62 -0
- data/lib/etassia/notation/plantuml/activity/metrics.rb +169 -0
- data/lib/etassia/notation/plantuml/activity/new_parser/blocks.rb +166 -0
- data/lib/etassia/notation/plantuml/activity/new_parser.rb +177 -0
- data/lib/etassia/notation/plantuml/activity/node.rb +50 -0
- data/lib/etassia/notation/plantuml/activity/node_marks.rb +174 -0
- data/lib/etassia/notation/plantuml/activity/note.rb +27 -0
- data/lib/etassia/notation/plantuml/activity/parser.rb +51 -0
- data/lib/etassia/notation/plantuml/activity/placement.rb +327 -0
- data/lib/etassia/notation/plantuml/activity/ranking.rb +142 -0
- data/lib/etassia/notation/plantuml/activity/reach.rb +58 -0
- data/lib/etassia/notation/plantuml/activity/renderer.rb +220 -0
- data/lib/etassia/notation/plantuml/activity/route.rb +22 -0
- data/lib/etassia/notation/plantuml/activity/router.rb +308 -0
- data/lib/etassia/notation/plantuml/activity/row_gaps.rb +105 -0
- data/lib/etassia/notation/plantuml/activity/row_spread.rb +59 -0
- data/lib/etassia/notation/plantuml/activity/scene.rb +20 -0
- data/lib/etassia/notation/plantuml/activity/text.rb +36 -0
- data/lib/etassia/notation/plantuml/activity.rb +50 -0
- data/lib/etassia/notation/plantuml/arrow.rb +135 -0
- data/lib/etassia/notation/plantuml/caption.rb +50 -0
- data/lib/etassia/notation/plantuml/caption_layout.rb +97 -0
- data/lib/etassia/notation/plantuml/caption_rows.rb +67 -0
- data/lib/etassia/notation/plantuml/class_header.rb +178 -0
- data/lib/etassia/notation/plantuml/class_name.rb +51 -0
- data/lib/etassia/notation/plantuml/component.rb +37 -0
- data/lib/etassia/notation/plantuml/creole.rb +278 -0
- data/lib/etassia/notation/plantuml/creole_icons.rb +33 -0
- data/lib/etassia/notation/plantuml/diagram.rb +60 -0
- data/lib/etassia/notation/plantuml/diagram_builder.rb +442 -0
- data/lib/etassia/notation/plantuml/diagram_name.rb +51 -0
- data/lib/etassia/notation/plantuml/directives.rb +33 -0
- data/lib/etassia/notation/plantuml/drawing/box.rb +28 -0
- data/lib/etassia/notation/plantuml/drawing/face.rb +39 -0
- data/lib/etassia/notation/plantuml/drawing/pen.rb +105 -0
- data/lib/etassia/notation/plantuml/drawing/renderer.rb +36 -0
- data/lib/etassia/notation/plantuml/drawing/scene.rb +28 -0
- data/lib/etassia/notation/plantuml/drawing.rb +19 -0
- data/lib/etassia/notation/plantuml/ebnf/blocks.rb +272 -0
- data/lib/etassia/notation/plantuml/ebnf/layout.rb +117 -0
- data/lib/etassia/notation/plantuml/ebnf/model.rb +22 -0
- data/lib/etassia/notation/plantuml/ebnf/reader.rb +131 -0
- data/lib/etassia/notation/plantuml/ebnf.rb +34 -0
- data/lib/etassia/notation/plantuml/end_texts.rb +78 -0
- data/lib/etassia/notation/plantuml/entity.rb +54 -0
- data/lib/etassia/notation/plantuml/further.rb +38 -0
- data/lib/etassia/notation/plantuml/gantt/layout.rb +269 -0
- data/lib/etassia/notation/plantuml/gantt/reader.rb +210 -0
- data/lib/etassia/notation/plantuml/gantt/renderer.rb +81 -0
- data/lib/etassia/notation/plantuml/gantt.rb +34 -0
- data/lib/etassia/notation/plantuml/include/blocks.rb +66 -0
- data/lib/etassia/notation/plantuml/include/finder.rb +61 -0
- data/lib/etassia/notation/plantuml/include.rb +78 -0
- data/lib/etassia/notation/plantuml/ir_adapter/annotations.rb +40 -0
- data/lib/etassia/notation/plantuml/ir_adapter/links.rb +91 -0
- data/lib/etassia/notation/plantuml/ir_adapter/sink.rb +49 -0
- data/lib/etassia/notation/plantuml/ir_adapter/structure.rb +86 -0
- data/lib/etassia/notation/plantuml/ir_adapter.rb +32 -0
- data/lib/etassia/notation/plantuml/ir_reader/annotations.rb +51 -0
- data/lib/etassia/notation/plantuml/ir_reader/index.rb +49 -0
- data/lib/etassia/notation/plantuml/ir_reader/links.rb +133 -0
- data/lib/etassia/notation/plantuml/ir_reader/structure.rb +83 -0
- data/lib/etassia/notation/plantuml/ir_reader.rb +42 -0
- data/lib/etassia/notation/plantuml/junction.rb +20 -0
- data/lib/etassia/notation/plantuml/klass.rb +41 -0
- data/lib/etassia/notation/plantuml/label_clearance.rb +67 -0
- data/lib/etassia/notation/plantuml/layout.rb +2806 -0
- data/lib/etassia/notation/plantuml/member.rb +37 -0
- data/lib/etassia/notation/plantuml/member_icon.rb +87 -0
- data/lib/etassia/notation/plantuml/member_icon_elements.rb +91 -0
- data/lib/etassia/notation/plantuml/member_pitch.rb +76 -0
- data/lib/etassia/notation/plantuml/member_rule.rb +48 -0
- data/lib/etassia/notation/plantuml/network/grid.rb +170 -0
- data/lib/etassia/notation/plantuml/network/layout.rb +158 -0
- data/lib/etassia/notation/plantuml/network/model.rb +22 -0
- data/lib/etassia/notation/plantuml/network/placement.rb +64 -0
- data/lib/etassia/notation/plantuml/network/reader.rb +126 -0
- data/lib/etassia/notation/plantuml/network/shapes.rb +109 -0
- data/lib/etassia/notation/plantuml/network.rb +37 -0
- data/lib/etassia/notation/plantuml/node_graph/archimate.rb +49 -0
- data/lib/etassia/notation/plantuml/node_graph/box_reader.rb +86 -0
- data/lib/etassia/notation/plantuml/node_graph/cloud.rb +73 -0
- data/lib/etassia/notation/plantuml/node_graph/crows_foot.rb +85 -0
- data/lib/etassia/notation/plantuml/node_graph/curve.rb +66 -0
- data/lib/etassia/notation/plantuml/node_graph/data_reader.rb +77 -0
- data/lib/etassia/notation/plantuml/node_graph/data_shape.rb +39 -0
- data/lib/etassia/notation/plantuml/node_graph/detour.rb +98 -0
- data/lib/etassia/notation/plantuml/node_graph/edge.rb +26 -0
- data/lib/etassia/notation/plantuml/node_graph/entity_metrics.rb +49 -0
- data/lib/etassia/notation/plantuml/node_graph/entity_reader.rb +65 -0
- data/lib/etassia/notation/plantuml/node_graph/entity_shape.rb +111 -0
- data/lib/etassia/notation/plantuml/node_graph/figures.rb +92 -0
- data/lib/etassia/notation/plantuml/node_graph/frame_ports.rb +73 -0
- data/lib/etassia/notation/plantuml/node_graph/graph.rb +40 -0
- data/lib/etassia/notation/plantuml/node_graph/heads.rb +89 -0
- data/lib/etassia/notation/plantuml/node_graph/label_fan.rb +76 -0
- data/lib/etassia/notation/plantuml/node_graph/label_placer.rb +310 -0
- data/lib/etassia/notation/plantuml/node_graph/label_waypoint.rb +47 -0
- data/lib/etassia/notation/plantuml/node_graph/layout.rb +293 -0
- data/lib/etassia/notation/plantuml/node_graph/link.rb +78 -0
- data/lib/etassia/notation/plantuml/node_graph/macros.rb +39 -0
- data/lib/etassia/notation/plantuml/node_graph/metrics.rb +113 -0
- data/lib/etassia/notation/plantuml/node_graph/node.rb +44 -0
- data/lib/etassia/notation/plantuml/node_graph/note_tail.rb +58 -0
- data/lib/etassia/notation/plantuml/node_graph/packer.rb +126 -0
- data/lib/etassia/notation/plantuml/node_graph/parser.rb +309 -0
- data/lib/etassia/notation/plantuml/node_graph/paths.rb +48 -0
- data/lib/etassia/notation/plantuml/node_graph/ranking.rb +231 -0
- data/lib/etassia/notation/plantuml/node_graph/reader.rb +53 -0
- data/lib/etassia/notation/plantuml/node_graph/references.rb +38 -0
- data/lib/etassia/notation/plantuml/node_graph/renderer.rb +160 -0
- data/lib/etassia/notation/plantuml/node_graph/scene.rb +22 -0
- data/lib/etassia/notation/plantuml/node_graph/shapes.rb +302 -0
- data/lib/etassia/notation/plantuml/node_graph/skipped_ranks.rb +61 -0
- data/lib/etassia/notation/plantuml/node_graph/tab.rb +68 -0
- data/lib/etassia/notation/plantuml/node_graph/usecase_ellipse.rb +100 -0
- data/lib/etassia/notation/plantuml/node_graph.rb +38 -0
- data/lib/etassia/notation/plantuml/note.rb +27 -0
- data/lib/etassia/notation/plantuml/note_layout.rb +257 -0
- data/lib/etassia/notation/plantuml/note_outline.rb +68 -0
- data/lib/etassia/notation/plantuml/objects/map_rows.rb +46 -0
- data/lib/etassia/notation/plantuml/objects/reader.rb +208 -0
- data/lib/etassia/notation/plantuml/objects.rb +39 -0
- data/lib/etassia/notation/plantuml/package.rb +45 -0
- data/lib/etassia/notation/plantuml/package_color.rb +32 -0
- data/lib/etassia/notation/plantuml/package_frames.rb +178 -0
- data/lib/etassia/notation/plantuml/parser.rb +622 -0
- data/lib/etassia/notation/plantuml/portion_visibility.rb +91 -0
- data/lib/etassia/notation/plantuml/qualified_association_layout.rb +332 -0
- data/lib/etassia/notation/plantuml/qualifier_box.rb +78 -0
- data/lib/etassia/notation/plantuml/rank_chain.rb +527 -0
- data/lib/etassia/notation/plantuml/rank_hierarchy.rb +347 -0
- data/lib/etassia/notation/plantuml/relation.rb +103 -0
- data/lib/etassia/notation/plantuml/relation_label_separator.rb +199 -0
- data/lib/etassia/notation/plantuml/relation_path_points.rb +84 -0
- data/lib/etassia/notation/plantuml/renderer.rb +490 -0
- data/lib/etassia/notation/plantuml/scene.rb +134 -0
- data/lib/etassia/notation/plantuml/segment_box.rb +85 -0
- data/lib/etassia/notation/plantuml/sequence/activation.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/appearance.rb +71 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_end.rb +23 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_marks.rb +111 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_style.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_syntax.rb +122 -0
- data/lib/etassia/notation/plantuml/sequence/bar_tracker.rb +59 -0
- data/lib/etassia/notation/plantuml/sequence/box.rb +23 -0
- data/lib/etassia/notation/plantuml/sequence/chrome.rb +60 -0
- data/lib/etassia/notation/plantuml/sequence/chrome_rows.rb +184 -0
- data/lib/etassia/notation/plantuml/sequence/create.rb +34 -0
- data/lib/etassia/notation/plantuml/sequence/delay.rb +19 -0
- data/lib/etassia/notation/plantuml/sequence/destroy.rb +20 -0
- data/lib/etassia/notation/plantuml/sequence/diagram.rb +86 -0
- data/lib/etassia/notation/plantuml/sequence/divider.rb +19 -0
- data/lib/etassia/notation/plantuml/sequence/edge.rb +53 -0
- data/lib/etassia/notation/plantuml/sequence/embedded.rb +77 -0
- data/lib/etassia/notation/plantuml/sequence/fill.rb +56 -0
- data/lib/etassia/notation/plantuml/sequence/fragment.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/fragment_shape.rb +138 -0
- data/lib/etassia/notation/plantuml/sequence/head_style.rb +43 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/blocks.rb +73 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/fills.rb +24 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/items.rb +48 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/messages.rb +62 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/participants.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/settings.rb +54 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter.rb +38 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/blocks.rb +77 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/fills.rb +27 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/index.rb +36 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/items.rb +35 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/messages.rb +70 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/participants.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/settings.rb +74 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader.rb +38 -0
- data/lib/etassia/notation/plantuml/sequence/keyword_defaults.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/layout.rb +1567 -0
- data/lib/etassia/notation/plantuml/sequence/message.rb +97 -0
- data/lib/etassia/notation/plantuml/sequence/message_wrap.rb +77 -0
- data/lib/etassia/notation/plantuml/sequence/note.rb +61 -0
- data/lib/etassia/notation/plantuml/sequence/note_fill.rb +35 -0
- data/lib/etassia/notation/plantuml/sequence/note_geometry.rb +126 -0
- data/lib/etassia/notation/plantuml/sequence/note_shape.rb +51 -0
- data/lib/etassia/notation/plantuml/sequence/numbered_label.rb +57 -0
- data/lib/etassia/notation/plantuml/sequence/numbering.rb +49 -0
- data/lib/etassia/notation/plantuml/sequence/outline.rb +204 -0
- data/lib/etassia/notation/plantuml/sequence/page_break.rb +17 -0
- data/lib/etassia/notation/plantuml/sequence/paint.rb +79 -0
- data/lib/etassia/notation/plantuml/sequence/parallel_message.rb +19 -0
- data/lib/etassia/notation/plantuml/sequence/parser.rb +844 -0
- data/lib/etassia/notation/plantuml/sequence/participant.rb +27 -0
- data/lib/etassia/notation/plantuml/sequence/picture.rb +29 -0
- data/lib/etassia/notation/plantuml/sequence/ref.rb +24 -0
- data/lib/etassia/notation/plantuml/sequence/ref_shape.rb +74 -0
- data/lib/etassia/notation/plantuml/sequence/refusals.rb +47 -0
- data/lib/etassia/notation/plantuml/sequence/renderer.rb +501 -0
- data/lib/etassia/notation/plantuml/sequence/scene.rb +200 -0
- data/lib/etassia/notation/plantuml/sequence/skin.rb +76 -0
- data/lib/etassia/notation/plantuml/sequence/spacing.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/style.rb +151 -0
- data/lib/etassia/notation/plantuml/sequence/tab_size.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/walker.rb +1190 -0
- data/lib/etassia/notation/plantuml/sequence.rb +47 -0
- data/lib/etassia/notation/plantuml/source_lines.rb +33 -0
- data/lib/etassia/notation/plantuml/state/axis_solver.rb +272 -0
- data/lib/etassia/notation/plantuml/state/cluster_padding.rb +75 -0
- data/lib/etassia/notation/plantuml/state/curve.rb +73 -0
- data/lib/etassia/notation/plantuml/state/dot_axis.rb +248 -0
- data/lib/etassia/notation/plantuml/state/extent.rb +42 -0
- data/lib/etassia/notation/plantuml/state/hierarchy.rb +355 -0
- data/lib/etassia/notation/plantuml/state/label_clearance.rb +148 -0
- data/lib/etassia/notation/plantuml/state/label_reach.rb +139 -0
- data/lib/etassia/notation/plantuml/state/layout.rb +173 -0
- data/lib/etassia/notation/plantuml/state/level_edges.rb +113 -0
- data/lib/etassia/notation/plantuml/state/look.rb +36 -0
- data/lib/etassia/notation/plantuml/state/note_shape.rb +73 -0
- data/lib/etassia/notation/plantuml/state/plane.rb +85 -0
- data/lib/etassia/notation/plantuml/state/ranking.rb +181 -0
- data/lib/etassia/notation/plantuml/state/reader.rb +310 -0
- data/lib/etassia/notation/plantuml/state/renderer.rb +253 -0
- data/lib/etassia/notation/plantuml/state/route_detour.rb +115 -0
- data/lib/etassia/notation/plantuml/state/route_geometry.rb +186 -0
- data/lib/etassia/notation/plantuml/state/routes.rb +196 -0
- data/lib/etassia/notation/plantuml/state/row.rb +88 -0
- data/lib/etassia/notation/plantuml/state/rows.rb +50 -0
- data/lib/etassia/notation/plantuml/state/stretch.rb +71 -0
- data/lib/etassia/notation/plantuml/state.rb +62 -0
- data/lib/etassia/notation/plantuml/stereotype_head.rb +58 -0
- data/lib/etassia/notation/plantuml/style/bound.rb +34 -0
- data/lib/etassia/notation/plantuml/style/colour.rb +41 -0
- data/lib/etassia/notation/plantuml/style/colour_names.rb +89 -0
- data/lib/etassia/notation/plantuml/style/defaults.rb +33 -0
- data/lib/etassia/notation/plantuml/style/inline.rb +56 -0
- data/lib/etassia/notation/plantuml/style/reader.rb +242 -0
- data/lib/etassia/notation/plantuml/style/sheet.rb +103 -0
- data/lib/etassia/notation/plantuml/style.rb +32 -0
- data/lib/etassia/notation/plantuml/style_reader.rb +160 -0
- data/lib/etassia/notation/plantuml/style_sheet.rb +27 -0
- data/lib/etassia/notation/plantuml/styled_relation_curve.rb +71 -0
- data/lib/etassia/notation/plantuml/timing/arrows.rb +81 -0
- data/lib/etassia/notation/plantuml/timing/axis.rb +100 -0
- data/lib/etassia/notation/plantuml/timing/layout.rb +172 -0
- data/lib/etassia/notation/plantuml/timing/model.rb +34 -0
- data/lib/etassia/notation/plantuml/timing/reader.rb +173 -0
- data/lib/etassia/notation/plantuml/timing/signals.rb +328 -0
- data/lib/etassia/notation/plantuml/timing.rb +38 -0
- data/lib/etassia/notation/plantuml/tree/layout.rb +276 -0
- data/lib/etassia/notation/plantuml/tree/mindmap_layout.rb +157 -0
- data/lib/etassia/notation/plantuml/tree/reader.rb +186 -0
- data/lib/etassia/notation/plantuml/tree/renderer.rb +67 -0
- data/lib/etassia/notation/plantuml/tree/role.rb +57 -0
- data/lib/etassia/notation/plantuml/tree/rules.rb +156 -0
- data/lib/etassia/notation/plantuml/tree/wbs_layout.rb +164 -0
- data/lib/etassia/notation/plantuml/tree.rb +41 -0
- data/lib/etassia/notation/plantuml/unsupported_construct_error.rb +39 -0
- data/lib/etassia/notation/plantuml/unsupported_constructs.rb +103 -0
- data/lib/etassia/notation/plantuml/usecase.rb +36 -0
- data/lib/etassia/notation/plantuml.rb +132 -0
- data/lib/etassia/notation/plantuml_data/boxes/frames.rb +101 -0
- data/lib/etassia/notation/plantuml_data/boxes/graph_layout.rb +348 -0
- data/lib/etassia/notation/plantuml_data/boxes/heads.rb +65 -0
- data/lib/etassia/notation/plantuml_data/boxes/highlights.rb +70 -0
- data/lib/etassia/notation/plantuml_data/boxes/inline_rows.rb +99 -0
- data/lib/etassia/notation/plantuml_data/boxes/json_reader.rb +24 -0
- data/lib/etassia/notation/plantuml_data/boxes/link_text.rb +94 -0
- data/lib/etassia/notation/plantuml_data/boxes/marks.rb +76 -0
- data/lib/etassia/notation/plantuml_data/boxes/ornaments.rb +130 -0
- data/lib/etassia/notation/plantuml_data/boxes/renderer.rb +95 -0
- data/lib/etassia/notation/plantuml_data/boxes/row_alignment.rb +58 -0
- data/lib/etassia/notation/plantuml_data/boxes/row_bend.rb +43 -0
- data/lib/etassia/notation/plantuml_data/boxes/scene.rb +27 -0
- data/lib/etassia/notation/plantuml_data/boxes/table.rb +56 -0
- data/lib/etassia/notation/plantuml_data/boxes/table_box.rb +355 -0
- data/lib/etassia/notation/plantuml_data/boxes/text.rb +101 -0
- data/lib/etassia/notation/plantuml_data/boxes/tree_builder.rb +82 -0
- data/lib/etassia/notation/plantuml_data/boxes/tree_layout.rb +82 -0
- data/lib/etassia/notation/plantuml_data/boxes/yaml_reader.rb +105 -0
- data/lib/etassia/notation/plantuml_data/openiconic.LICENSE +6 -0
- data/lib/etassia/notation/plantuml_data/openiconic.json +1 -0
- data/lib/etassia/notation/plantuml_data.rb +84 -0
- data/lib/etassia/notation/plantuml_salt/block.rb +80 -0
- data/lib/etassia/notation/plantuml_salt/box.rb +30 -0
- data/lib/etassia/notation/plantuml_salt/font_metrics.rb +28 -0
- data/lib/etassia/notation/plantuml_salt/grid_marks.rb +66 -0
- data/lib/etassia/notation/plantuml_salt/group_marks.rb +77 -0
- data/lib/etassia/notation/plantuml_salt/layout.rb +206 -0
- data/lib/etassia/notation/plantuml_salt/mark.rb +36 -0
- data/lib/etassia/notation/plantuml_salt/markup.rb +57 -0
- data/lib/etassia/notation/plantuml_salt/parser.rb +239 -0
- data/lib/etassia/notation/plantuml_salt/renderer.rb +138 -0
- data/lib/etassia/notation/plantuml_salt/row.rb +19 -0
- data/lib/etassia/notation/plantuml_salt/run.rb +34 -0
- data/lib/etassia/notation/plantuml_salt/scene.rb +23 -0
- data/lib/etassia/notation/plantuml_salt/scroll_marks.rb +94 -0
- data/lib/etassia/notation/plantuml_salt/tab_marks.rb +74 -0
- data/lib/etassia/notation/plantuml_salt/tree_marks.rb +127 -0
- data/lib/etassia/notation/plantuml_salt/widget.rb +40 -0
- data/lib/etassia/notation/plantuml_salt/widget_marks.rb +319 -0
- data/lib/etassia/notation/plantuml_salt.rb +66 -0
- data/lib/etassia/notation/plugin_failure.rb +42 -0
- data/lib/etassia/notation.rb +289 -0
- data/lib/etassia/parser/architecture.rb +25 -0
- data/lib/etassia/parser/atoms/delimited_run.rb +82 -0
- data/lib/etassia/parser/atoms/greedy_run.rb +116 -0
- data/lib/etassia/parser/atoms/joined.rb +47 -0
- data/lib/etassia/parser/atoms/trimmed_run.rb +47 -0
- data/lib/etassia/parser/base.rb +258 -0
- data/lib/etassia/parser/block.rb +32 -0
- data/lib/etassia/parser/builders/architecture.rb +135 -0
- data/lib/etassia/parser/builders/block.rb +288 -0
- data/lib/etassia/parser/builders/c4.rb +275 -0
- data/lib/etassia/parser/builders/capture_string.rb +17 -0
- data/lib/etassia/parser/builders/class_diagram.rb +539 -0
- data/lib/etassia/parser/builders/er_diagram.rb +308 -0
- data/lib/etassia/parser/builders/error.rb +51 -0
- data/lib/etassia/parser/builders/flowchart.rb +1150 -0
- data/lib/etassia/parser/builders/gantt.rb +267 -0
- data/lib/etassia/parser/builders/git_graph.rb +301 -0
- data/lib/etassia/parser/builders/info.rb +57 -0
- data/lib/etassia/parser/builders/kanban.rb +411 -0
- data/lib/etassia/parser/builders/mindmap.rb +271 -0
- data/lib/etassia/parser/builders/packet.rb +85 -0
- data/lib/etassia/parser/builders/pie.rb +105 -0
- data/lib/etassia/parser/builders/quadrant.rb +165 -0
- data/lib/etassia/parser/builders/radar.rb +157 -0
- data/lib/etassia/parser/builders/requirement.rb +230 -0
- data/lib/etassia/parser/builders/sankey.rb +142 -0
- data/lib/etassia/parser/builders/sequence.rb +431 -0
- data/lib/etassia/parser/builders/sequence_blocks.rb +117 -0
- data/lib/etassia/parser/builders/sequence_box_title.rb +57 -0
- data/lib/etassia/parser/builders/state_diagram.rb +364 -0
- data/lib/etassia/parser/builders/timeline.rb +133 -0
- data/lib/etassia/parser/builders/treemap.rb +138 -0
- data/lib/etassia/parser/builders/xy_chart.rb +148 -0
- data/lib/etassia/parser/c4.rb +37 -0
- data/lib/etassia/parser/class_diagram.rb +58 -0
- data/lib/etassia/parser/class_namespaces.rb +62 -0
- data/lib/etassia/parser/class_notes.rb +54 -0
- data/lib/etassia/parser/er_diagram.rb +28 -0
- data/lib/etassia/parser/error.rb +30 -0
- data/lib/etassia/parser/flowchart.rb +87 -0
- data/lib/etassia/parser/gantt.rb +43 -0
- data/lib/etassia/parser/git_graph.rb +106 -0
- data/lib/etassia/parser/grammars/architecture.rb +165 -0
- data/lib/etassia/parser/grammars/block.rb +190 -0
- data/lib/etassia/parser/grammars/c4.rb +229 -0
- data/lib/etassia/parser/grammars/class_diagram.rb +606 -0
- data/lib/etassia/parser/grammars/common.rb +107 -0
- data/lib/etassia/parser/grammars/er_diagram.rb +232 -0
- data/lib/etassia/parser/grammars/error.rb +32 -0
- data/lib/etassia/parser/grammars/flowchart.rb +1722 -0
- data/lib/etassia/parser/grammars/gantt.rb +198 -0
- data/lib/etassia/parser/grammars/git_graph.rb +234 -0
- data/lib/etassia/parser/grammars/info.rb +53 -0
- data/lib/etassia/parser/grammars/kanban.rb +417 -0
- data/lib/etassia/parser/grammars/mermaid_unicode_text.rb +108 -0
- data/lib/etassia/parser/grammars/mindmap.rb +220 -0
- data/lib/etassia/parser/grammars/packet.rb +71 -0
- data/lib/etassia/parser/grammars/pie.rb +128 -0
- data/lib/etassia/parser/grammars/quadrant.rb +202 -0
- data/lib/etassia/parser/grammars/radar.rb +158 -0
- data/lib/etassia/parser/grammars/requirement.rb +258 -0
- data/lib/etassia/parser/grammars/sankey.rb +104 -0
- data/lib/etassia/parser/grammars/sequence.rb +940 -0
- data/lib/etassia/parser/grammars/state_diagram.rb +382 -0
- data/lib/etassia/parser/grammars/timeline.rb +153 -0
- data/lib/etassia/parser/grammars/treemap.rb +122 -0
- data/lib/etassia/parser/grammars/xy_chart.rb +119 -0
- data/lib/etassia/parser/info.rb +32 -0
- data/lib/etassia/parser/kanban.rb +131 -0
- data/lib/etassia/parser/mermaid_shapes.rb +106 -0
- data/lib/etassia/parser/metadata_yaml.rb +452 -0
- data/lib/etassia/parser/mindmap.rb +92 -0
- data/lib/etassia/parser/native/builders/architecture.rb +103 -0
- data/lib/etassia/parser/native/builders/block.rb +68 -0
- data/lib/etassia/parser/native/builders/c4.rb +16 -0
- data/lib/etassia/parser/native/builders/class_diagram.rb +63 -0
- data/lib/etassia/parser/native/builders/er_diagram.rb +47 -0
- data/lib/etassia/parser/native/builders/flowchart.rb +58 -0
- data/lib/etassia/parser/native/builders/gantt.rb +35 -0
- data/lib/etassia/parser/native/builders/gitgraph.rb +35 -0
- data/lib/etassia/parser/native/builders/info.rb +30 -0
- data/lib/etassia/parser/native/builders/kanban.rb +37 -0
- data/lib/etassia/parser/native/builders/mindmap.rb +26 -0
- data/lib/etassia/parser/native/builders/packet.rb +63 -0
- data/lib/etassia/parser/native/builders/pie.rb +82 -0
- data/lib/etassia/parser/native/builders/quadrant.rb +27 -0
- data/lib/etassia/parser/native/builders/radar.rb +84 -0
- data/lib/etassia/parser/native/builders/requirement.rb +25 -0
- data/lib/etassia/parser/native/builders/sankey.rb +18 -0
- data/lib/etassia/parser/native/builders/sequence.rb +48 -0
- data/lib/etassia/parser/native/builders/state_diagram.rb +61 -0
- data/lib/etassia/parser/native/builders/timeline.rb +46 -0
- data/lib/etassia/parser/native/builders/treemap.rb +60 -0
- data/lib/etassia/parser/native/builders/user_journey.rb +83 -0
- data/lib/etassia/parser/native/builders/xy_chart.rb +94 -0
- data/lib/etassia/parser/native/declined.rb +12 -0
- data/lib/etassia/parser/native/mermaid.json +1 -0
- data/lib/etassia/parser/native/text.rb +31 -0
- data/lib/etassia/parser/native.rb +99 -0
- data/lib/etassia/parser/packet.rb +56 -0
- data/lib/etassia/parser/pie.rb +32 -0
- data/lib/etassia/parser/quadrant.rb +77 -0
- data/lib/etassia/parser/radar.rb +99 -0
- data/lib/etassia/parser/requirement.rb +32 -0
- data/lib/etassia/parser/sankey.rb +47 -0
- data/lib/etassia/parser/sequence.rb +34 -0
- data/lib/etassia/parser/state_diagram.rb +29 -0
- data/lib/etassia/parser/timeline.rb +40 -0
- data/lib/etassia/parser/treemap.rb +25 -0
- data/lib/etassia/parser/user_journey.rb +253 -0
- data/lib/etassia/parser/xy_chart.rb +98 -0
- data/lib/etassia/parser.rb +18 -0
- data/lib/etassia/renderer/architecture.rb +232 -0
- data/lib/etassia/renderer/architecture_edge_router.rb +379 -0
- data/lib/etassia/renderer/base.rb +295 -0
- data/lib/etassia/renderer/block.rb +157 -0
- data/lib/etassia/renderer/c4.rb +294 -0
- data/lib/etassia/renderer/class_diagram.rb +671 -0
- data/lib/etassia/renderer/er_diagram.rb +842 -0
- data/lib/etassia/renderer/error.rb +131 -0
- data/lib/etassia/renderer/flowchart.rb +315 -0
- data/lib/etassia/renderer/gantt.rb +177 -0
- data/lib/etassia/renderer/git_graph.rb +224 -0
- data/lib/etassia/renderer/info.rb +59 -0
- data/lib/etassia/renderer/kanban.rb +157 -0
- data/lib/etassia/renderer/line_break_text.rb +56 -0
- data/lib/etassia/renderer/markdown_text.rb +106 -0
- data/lib/etassia/renderer/mindmap.rb +290 -0
- data/lib/etassia/renderer/packet.rb +87 -0
- data/lib/etassia/renderer/pie.rb +113 -0
- data/lib/etassia/renderer/quadrant.rb +189 -0
- data/lib/etassia/renderer/radar.rb +191 -0
- data/lib/etassia/renderer/requirement.rb +209 -0
- data/lib/etassia/renderer/sankey.rb +114 -0
- data/lib/etassia/renderer/sequence/frame_drawing.rb +169 -0
- data/lib/etassia/renderer/sequence/note_drawing.rb +69 -0
- data/lib/etassia/renderer/sequence.rb +490 -0
- data/lib/etassia/renderer/state_diagram.rb +202 -0
- data/lib/etassia/renderer/timeline.rb +134 -0
- data/lib/etassia/renderer/timeline_palette.rb +18 -0
- data/lib/etassia/renderer/treemap/section_total.rb +33 -0
- data/lib/etassia/renderer/treemap.rb +128 -0
- data/lib/etassia/renderer/user_journey.rb +209 -0
- data/lib/etassia/renderer/xy_chart.rb +180 -0
- data/lib/etassia/renderer.rb +22 -0
- data/lib/etassia/runtime.rb +125 -0
- data/lib/etassia/source/frontmatter.rb +501 -0
- data/lib/etassia/source.rb +224 -0
- data/lib/etassia/svg/arrowhead.rb +205 -0
- data/lib/etassia/svg/circle.rb +19 -0
- data/lib/etassia/svg/document.rb +106 -0
- data/lib/etassia/svg/element.rb +209 -0
- data/lib/etassia/svg/ellipse.rb +23 -0
- data/lib/etassia/svg/escaping.rb +149 -0
- data/lib/etassia/svg/group.rb +64 -0
- data/lib/etassia/svg/line.rb +21 -0
- data/lib/etassia/svg/numbers.rb +53 -0
- data/lib/etassia/svg/path.rb +67 -0
- data/lib/etassia/svg/path_geometry.rb +378 -0
- data/lib/etassia/svg/polygon.rb +25 -0
- data/lib/etassia/svg/polyline.rb +28 -0
- data/lib/etassia/svg/rect.rb +34 -0
- data/lib/etassia/svg/style.rb +50 -0
- data/lib/etassia/svg/text.rb +181 -0
- data/lib/etassia/svg/tspan.rb +48 -0
- data/lib/etassia/svg.rb +38 -0
- data/lib/etassia/text_measurement/arial_advances.rb +122 -0
- data/lib/etassia/text_measurement/dejavu_advances.rb +111 -0
- data/lib/etassia/text_measurement/times_advances.rb +64 -0
- data/lib/etassia/text_measurement/trebuchet_advances.rb +53 -0
- data/lib/etassia/text_measurement/trebuchet_kerning.rb +29 -0
- data/lib/etassia/text_measurement.rb +198 -0
- data/lib/etassia/theme/builtin/dark.yml +229 -0
- data/lib/etassia/theme/builtin/default.yml +239 -0
- data/lib/etassia/theme/builtin/high_contrast.yml +231 -0
- data/lib/etassia/theme/builtin/light.yml +229 -0
- data/lib/etassia/theme/color_palette.rb +97 -0
- data/lib/etassia/theme/completion.rb +50 -0
- data/lib/etassia/theme/effect_styles.rb +34 -0
- data/lib/etassia/theme/registry.rb +45 -0
- data/lib/etassia/theme/shape_styles.rb +34 -0
- data/lib/etassia/theme/spacing_config.rb +30 -0
- data/lib/etassia/theme/typography.rb +40 -0
- data/lib/etassia/theme.rb +73 -0
- data/lib/etassia/version.rb +5 -0
- data/lib/etassia.rb +40 -0
- metadata +865 -0
|
@@ -0,0 +1,2806 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../layout/base"
|
|
4
|
+
require_relative "caption_layout"
|
|
5
|
+
require_relative "class_name"
|
|
6
|
+
require_relative "stereotype_head"
|
|
7
|
+
require_relative "class_header"
|
|
8
|
+
require_relative "end_texts"
|
|
9
|
+
require_relative "member_icon"
|
|
10
|
+
require_relative "member_pitch"
|
|
11
|
+
require_relative "member_rule"
|
|
12
|
+
require_relative "ir_reader"
|
|
13
|
+
require_relative "package_frames"
|
|
14
|
+
require_relative "qualified_association_layout"
|
|
15
|
+
require_relative "qualifier_box"
|
|
16
|
+
require_relative "label_clearance"
|
|
17
|
+
require_relative "styled_relation_curve"
|
|
18
|
+
require_relative "rank_chain"
|
|
19
|
+
require_relative "rank_hierarchy"
|
|
20
|
+
require_relative "note_layout"
|
|
21
|
+
require_relative "relation_label_separator"
|
|
22
|
+
require_relative "scene"
|
|
23
|
+
require_relative "drawing"
|
|
24
|
+
require_relative "../../js_math"
|
|
25
|
+
|
|
26
|
+
module Etassia
|
|
27
|
+
module Notation
|
|
28
|
+
module PlantUML
|
|
29
|
+
# Positions PlantUML class boxes and relations in final canvas space,
|
|
30
|
+
# from the IR::Graph {IRAdapter} emits.
|
|
31
|
+
class Layout < Etassia::Layout::Base
|
|
32
|
+
MARGIN = 7.0
|
|
33
|
+
COLUMN_GAP = 90.0
|
|
34
|
+
ROW_GAP = 90.0
|
|
35
|
+
ANNOTATED_RELATION_SLOT_WIDTH = 160.0
|
|
36
|
+
BOX_PADDING = 12.0
|
|
37
|
+
ROW_HEIGHT = 22.0
|
|
38
|
+
# A compact box, sized from the reference row pitch, draws its member
|
|
39
|
+
# rows from the bottom edge: last baseline 6.95 above it, the head
|
|
40
|
+
# rule 17.5 above the first baseline, 8.0 between a field group and
|
|
41
|
+
# a method group.
|
|
42
|
+
COMPACT_BOTTOM_PAD = 6.95
|
|
43
|
+
COMPACT_HEAD_GAP = 17.5
|
|
44
|
+
COMPACT_GROUP_GAP = 8.0
|
|
45
|
+
# The head rule is 32 below the top and the first row 17.5 below it,
|
|
46
|
+
# the last row 6.95 above the bottom, less the pitch of that row.
|
|
47
|
+
PROPORTIONAL_MEMBER_BASE_HEIGHT = 40.0
|
|
48
|
+
COMPACT_RULE_BASE_HEIGHT = 34.488
|
|
49
|
+
MARKER_LENGTH = 12.0
|
|
50
|
+
THICK_LINE = 4.0
|
|
51
|
+
JUNCTION_DOT_RADIUS = 2.0
|
|
52
|
+
MARKER_HALF_WIDTH = 7.0
|
|
53
|
+
LABEL_OFFSET = 10.0
|
|
54
|
+
LABEL_GAP = 2.0
|
|
55
|
+
BESIDE_LABEL_SLOPE = 0.8
|
|
56
|
+
END_TEXT_ACROSS = 3.0
|
|
57
|
+
END_TEXT_ALONG = 1.0
|
|
58
|
+
TEXT_DESCENT = 0.25
|
|
59
|
+
# The renderer draws end texts and relation labels at this share of
|
|
60
|
+
# the body font size.
|
|
61
|
+
SMALL_TEXT_SCALE = 0.9
|
|
62
|
+
NESTING_RADIUS = 8.0
|
|
63
|
+
MEMBER_NOTE_TEXT_SCALE = 1.15
|
|
64
|
+
MEMBER_NOTE_ROW_HEIGHT = 16.48828125
|
|
65
|
+
MEMBER_NOTE_LINE_HEIGHT = 15.310546875
|
|
66
|
+
MEMBER_NOTE_STACK_GAP = 10.0
|
|
67
|
+
MEMBER_NOTE_BODY_GAP = 35.0
|
|
68
|
+
CLASS_NOTE_GAP = 35.0
|
|
69
|
+
MEMBER_NOTE_CLASS_HEIGHT = 48.0
|
|
70
|
+
MEMBER_NOTE_SIDE_PADDING = 6.0
|
|
71
|
+
STANDARD_MEMBER_SIDE_ALLOWANCE = 20.0
|
|
72
|
+
STANDARD_STEREOTYPE_ROW_HEIGHT = 8.62109375
|
|
73
|
+
STANDARD_TITLE_SIDE_ALLOWANCE = 32.0
|
|
74
|
+
MULTILINE_TITLE_ROW_HEIGHT = 16.48828125
|
|
75
|
+
MULTILINE_TITLE_VERTICAL_ALLOWANCE = 26.0
|
|
76
|
+
MULTILINE_TITLE_BASELINE = 18.53515625
|
|
77
|
+
MULTILINE_TITLE_SIDE_ALLOWANCE = 36.0
|
|
78
|
+
SELF_RELATION_START_RATIO = 0.37
|
|
79
|
+
SELF_RELATION_END_RATIO = 0.635
|
|
80
|
+
SELF_RELATION_CONTROL_OFFSET = 70.0
|
|
81
|
+
SELF_RELATION_LABEL_GAP = 6.0
|
|
82
|
+
# PlantUML removes the otherwise empty member compartment below a
|
|
83
|
+
# one-line title when `hide empty members` is active. Its Smetana
|
|
84
|
+
# boxes also keep four more pixels of side padding than our ordinary
|
|
85
|
+
# measured class box; both values are pinned by the group2712 family.
|
|
86
|
+
EMPTY_MEMBER_COMPARTMENT = 14.0
|
|
87
|
+
EMPTY_COMPARTMENTS_EXTRA = 2.0
|
|
88
|
+
AXIS_TOLERANCE = 0.5
|
|
89
|
+
EMPTY_CLASS_WIDTH_EXTRA = 8.0
|
|
90
|
+
EMPTY_CLASS_MIN_WIDTH = 41.5
|
|
91
|
+
HIDE_CIRCLE_BASE_HEIGHT = 42.48828125
|
|
92
|
+
HIDE_CIRCLE_TITLE_ALLOWANCE = 6.0
|
|
93
|
+
HIDE_CIRCLE_MEMBER_ALLOWANCE = 12.0
|
|
94
|
+
ISOLATED_KIND_COLUMN_GAP = 35.0
|
|
95
|
+
ISOLATED_KIND_ROW_GAP = 60.0
|
|
96
|
+
ISOLATED_KIND_MEMBER_ALLOWANCE = 26.0
|
|
97
|
+
HIDDEN_MEMBER_EMPTY_HEIGHT = 32.0
|
|
98
|
+
RESHOWN_MEMBER_BASE_HEIGHT = 40.0
|
|
99
|
+
QUALIFIED_GENERIC_FONT_SIZE = 12.0
|
|
100
|
+
QUALIFIED_GENERIC_GAP = 7.0
|
|
101
|
+
QUALIFIED_BOX_HEIGHT = 48.0
|
|
102
|
+
QUALIFIED_GENERIC_HEIGHT = 51.0
|
|
103
|
+
QUALIFIED_QUALIFIER_ALLOWANCE = 40.0
|
|
104
|
+
COMPACT_QUALIFIER_ALLOWANCE = 20.0
|
|
105
|
+
QUALIFIED_STEEP_ARROW_OFFSET = 37.0
|
|
106
|
+
QUALIFIED_LONG_CURVE_OFFSET = 210.0
|
|
107
|
+
QUALIFIED_FLAT_CURVE_OFFSET = 30.0
|
|
108
|
+
QUALIFIED_FAN_LANE_ALLOWANCE = 4.0
|
|
109
|
+
QUALIFIED_DOUBLE_END_HALF_WIDTH = 10.0
|
|
110
|
+
QUALIFIED_INHERITANCE_HALF_WIDTH = 6.0
|
|
111
|
+
PACKAGE_CANVAS_MARGIN = 6.0
|
|
112
|
+
PACKAGE_OUTER_GAP = 24.0
|
|
113
|
+
PACKAGE_CANVAS_EDGE = 15.0
|
|
114
|
+
ARROW_LENGTH = 9.0
|
|
115
|
+
ARROW_HALF_WIDTH = 4.0
|
|
116
|
+
ARROW_NOTCH = 5.0
|
|
117
|
+
DIAMOND_LENGTH = 12.0
|
|
118
|
+
DIAMOND_HALF_WIDTH = 4.0
|
|
119
|
+
ARROW_MARKERS = %i[association dependency].freeze
|
|
120
|
+
FILLED_MARKERS = %i[association dependency composition].freeze
|
|
121
|
+
private_constant :PROPORTIONAL_MEMBER_BASE_HEIGHT,
|
|
122
|
+
:COMPACT_BOTTOM_PAD, :COMPACT_HEAD_GAP,
|
|
123
|
+
:COMPACT_GROUP_GAP, :COMPACT_RULE_BASE_HEIGHT
|
|
124
|
+
private_constant :MARGIN, :COLUMN_GAP, :ROW_GAP,
|
|
125
|
+
:ANNOTATED_RELATION_SLOT_WIDTH, :BOX_PADDING,
|
|
126
|
+
:ROW_HEIGHT, :MARKER_LENGTH, :MARKER_HALF_WIDTH,
|
|
127
|
+
:LABEL_OFFSET, :LABEL_GAP, :FILLED_MARKERS,
|
|
128
|
+
:NESTING_RADIUS,
|
|
129
|
+
:END_TEXT_ACROSS, :END_TEXT_ALONG, :TEXT_DESCENT,
|
|
130
|
+
:SMALL_TEXT_SCALE,
|
|
131
|
+
:ARROW_LENGTH, :ARROW_HALF_WIDTH, :ARROW_NOTCH,
|
|
132
|
+
:DIAMOND_LENGTH, :DIAMOND_HALF_WIDTH,
|
|
133
|
+
:ARROW_MARKERS,
|
|
134
|
+
:MEMBER_NOTE_TEXT_SCALE, :MEMBER_NOTE_ROW_HEIGHT,
|
|
135
|
+
:MEMBER_NOTE_LINE_HEIGHT, :MEMBER_NOTE_STACK_GAP,
|
|
136
|
+
:MEMBER_NOTE_BODY_GAP, :MEMBER_NOTE_CLASS_HEIGHT,
|
|
137
|
+
:MEMBER_NOTE_SIDE_PADDING,
|
|
138
|
+
:CLASS_NOTE_GAP,
|
|
139
|
+
:STANDARD_MEMBER_SIDE_ALLOWANCE,
|
|
140
|
+
:STANDARD_STEREOTYPE_ROW_HEIGHT,
|
|
141
|
+
:STANDARD_TITLE_SIDE_ALLOWANCE,
|
|
142
|
+
:MULTILINE_TITLE_ROW_HEIGHT,
|
|
143
|
+
:MULTILINE_TITLE_VERTICAL_ALLOWANCE,
|
|
144
|
+
:MULTILINE_TITLE_BASELINE,
|
|
145
|
+
:MULTILINE_TITLE_SIDE_ALLOWANCE,
|
|
146
|
+
:SELF_RELATION_START_RATIO,
|
|
147
|
+
:SELF_RELATION_END_RATIO,
|
|
148
|
+
:SELF_RELATION_CONTROL_OFFSET,
|
|
149
|
+
:SELF_RELATION_LABEL_GAP,
|
|
150
|
+
:EMPTY_MEMBER_COMPARTMENT,
|
|
151
|
+
:EMPTY_COMPARTMENTS_EXTRA, :AXIS_TOLERANCE,
|
|
152
|
+
:EMPTY_CLASS_WIDTH_EXTRA,
|
|
153
|
+
:EMPTY_CLASS_MIN_WIDTH, :HIDE_CIRCLE_BASE_HEIGHT,
|
|
154
|
+
:HIDE_CIRCLE_TITLE_ALLOWANCE,
|
|
155
|
+
:HIDE_CIRCLE_MEMBER_ALLOWANCE,
|
|
156
|
+
:ISOLATED_KIND_COLUMN_GAP,
|
|
157
|
+
:ISOLATED_KIND_ROW_GAP,
|
|
158
|
+
:ISOLATED_KIND_MEMBER_ALLOWANCE,
|
|
159
|
+
:HIDDEN_MEMBER_EMPTY_HEIGHT,
|
|
160
|
+
:RESHOWN_MEMBER_BASE_HEIGHT,
|
|
161
|
+
:QUALIFIED_GENERIC_FONT_SIZE,
|
|
162
|
+
:QUALIFIED_GENERIC_GAP, :QUALIFIED_BOX_HEIGHT,
|
|
163
|
+
:QUALIFIED_GENERIC_HEIGHT,
|
|
164
|
+
:QUALIFIED_QUALIFIER_ALLOWANCE,
|
|
165
|
+
:COMPACT_QUALIFIER_ALLOWANCE,
|
|
166
|
+
:QUALIFIED_STEEP_ARROW_OFFSET,
|
|
167
|
+
:QUALIFIED_LONG_CURVE_OFFSET,
|
|
168
|
+
:QUALIFIED_FLAT_CURVE_OFFSET,
|
|
169
|
+
:QUALIFIED_FAN_LANE_ALLOWANCE,
|
|
170
|
+
:QUALIFIED_DOUBLE_END_HALF_WIDTH,
|
|
171
|
+
:QUALIFIED_INHERITANCE_HALF_WIDTH,
|
|
172
|
+
:PACKAGE_CANVAS_MARGIN, :PACKAGE_OUTER_GAP,
|
|
173
|
+
:PACKAGE_CANVAS_EDGE
|
|
174
|
+
|
|
175
|
+
# @param graph [IR::Graph] from {IRAdapter}
|
|
176
|
+
def scene(graph)
|
|
177
|
+
diagram = IRReader.call(graph)
|
|
178
|
+
@kind_in_spot = !ClassHeader.circles_hidden?(diagram)
|
|
179
|
+
body = if member_note_family?(diagram)
|
|
180
|
+
member_note_scene(diagram)
|
|
181
|
+
elsif class_note_pair_family?(diagram)
|
|
182
|
+
class_note_pair_scene(diagram)
|
|
183
|
+
else
|
|
184
|
+
standard_scene(diagram)
|
|
185
|
+
end
|
|
186
|
+
dress(diagram, body)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def standard_scene(diagram)
|
|
192
|
+
specifications = box_specifications(diagram)
|
|
193
|
+
hidden_members = hide_members_grid_scene(diagram, specifications)
|
|
194
|
+
return hidden_members if hidden_members
|
|
195
|
+
|
|
196
|
+
isolated_kinds = isolated_class_kind_scene(diagram, specifications)
|
|
197
|
+
return isolated_kinds if isolated_kinds
|
|
198
|
+
|
|
199
|
+
qualified = qualified_scene(diagram, specifications)
|
|
200
|
+
return qualified if qualified
|
|
201
|
+
|
|
202
|
+
standard_box_scene(diagram, specifications)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def hide_members_grid_scene(diagram, specifications)
|
|
206
|
+
return unless hide_members_grid_family?(diagram)
|
|
207
|
+
|
|
208
|
+
visible, hidden = specifications.partition do |item|
|
|
209
|
+
item[:member_rows].any?
|
|
210
|
+
end
|
|
211
|
+
ordered = visible + hidden
|
|
212
|
+
rows = ordered.each_slice(2).to_a
|
|
213
|
+
boxes = hide_members_grid_boxes(rows)
|
|
214
|
+
width, height = chain_dimensions(boxes)
|
|
215
|
+
Scene.new(width: width, height: height, boxes: boxes,
|
|
216
|
+
relations: [], frames: [])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def hide_members_grid_family?(diagram)
|
|
220
|
+
diagram.classes.size > 1 && diagram.notes.empty? &&
|
|
221
|
+
standalone_diagram?(diagram) &&
|
|
222
|
+
diagram.directives.include?("hide members") &&
|
|
223
|
+
diagram.classes.any? { |klass| klass.body.any? }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def hide_members_grid_boxes(rows)
|
|
227
|
+
row_widths = rows.map { |row| hide_members_row_width(row) }
|
|
228
|
+
widest = row_widths.max
|
|
229
|
+
tops = isolated_class_kind_row_tops(rows)
|
|
230
|
+
rows.each_with_index.flat_map do |row, index|
|
|
231
|
+
left = MARGIN + ((widest - row_widths.fetch(index)) / 2)
|
|
232
|
+
hide_members_grid_row(row, left, tops.fetch(index))
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def hide_members_row_width(row)
|
|
237
|
+
row.sum { |item| item[:width] } +
|
|
238
|
+
(ISOLATED_KIND_COLUMN_GAP * (row.size - 1))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def hide_members_grid_row(row, left, top)
|
|
242
|
+
height = row.map { |item| item[:height] }.max
|
|
243
|
+
row.each_with_object([]) do |item, boxes|
|
|
244
|
+
y = top + ((height - item[:height]) / 2)
|
|
245
|
+
boxes << build_box(item, left, y, item[:width])
|
|
246
|
+
left += item[:width] + ISOLATED_KIND_COLUMN_GAP
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def isolated_class_kind_scene(diagram, specifications)
|
|
251
|
+
return unless isolated_class_kind_family?(diagram) ||
|
|
252
|
+
loose_member_class_family?(diagram)
|
|
253
|
+
|
|
254
|
+
rows = specifications.each_slice(2).to_a
|
|
255
|
+
boxes = isolated_class_kind_boxes(rows)
|
|
256
|
+
width, height = chain_dimensions(boxes)
|
|
257
|
+
Scene.new(width: width, height: height, boxes: boxes,
|
|
258
|
+
relations: [], frames: [])
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def isolated_class_kind_family?(diagram)
|
|
262
|
+
diagram.classes.size > 1 && diagram.notes.empty? &&
|
|
263
|
+
standalone_diagram?(diagram) &&
|
|
264
|
+
diagram.classes.any? { |klass| klass.kind != :class }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Several unlinked classes with members: the jar packs them like
|
|
268
|
+
# the other loose boxes, a node-separation apart, not a grid column.
|
|
269
|
+
def loose_member_class_family?(diagram)
|
|
270
|
+
diagram.classes.size > 1 && diagram.notes.empty? &&
|
|
271
|
+
standalone_diagram?(diagram) &&
|
|
272
|
+
diagram.classes.any? { |klass| klass.body.any? }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def isolated_class_kind_boxes(rows)
|
|
276
|
+
widths = isolated_class_kind_column_widths(rows)
|
|
277
|
+
tops = isolated_class_kind_row_tops(rows)
|
|
278
|
+
rows.each_with_index.flat_map do |row, row_index|
|
|
279
|
+
isolated_class_kind_row(row, widths, tops.fetch(row_index))
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def isolated_class_kind_column_widths(rows)
|
|
284
|
+
columns = rows.map(&:size).max
|
|
285
|
+
Array.new(columns) do |column|
|
|
286
|
+
rows.filter_map { |row| row[column]&.fetch(:width) }.max
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def isolated_class_kind_row_tops(rows)
|
|
291
|
+
heights = rows.map { |row| row.map { |item| item[:height] }.max }
|
|
292
|
+
heights.each_with_object([MARGIN]) do |height, tops|
|
|
293
|
+
tops << (tops.last + height + ISOLATED_KIND_ROW_GAP)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def isolated_class_kind_row(row, widths, top)
|
|
298
|
+
lefts = isolated_class_kind_column_lefts(widths)
|
|
299
|
+
height = row.map { |item| item[:height] }.max
|
|
300
|
+
row.each_with_index.map do |item, column|
|
|
301
|
+
origin = isolated_class_kind_origin(item, lefts.fetch(column),
|
|
302
|
+
widths.fetch(column),
|
|
303
|
+
[top, height])
|
|
304
|
+
build_box(item, *origin, item[:width])
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def isolated_class_kind_origin(item, left, cell_width, (top, height))
|
|
309
|
+
[left + ((cell_width - item[:width]) / 2),
|
|
310
|
+
top + ((height - item[:height]) / 2)]
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def isolated_class_kind_column_lefts(widths)
|
|
314
|
+
widths.each_with_object([MARGIN]) do |width, lefts|
|
|
315
|
+
lefts << (lefts.last + width + ISOLATED_KIND_COLUMN_GAP)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def standard_box_scene(diagram, specifications)
|
|
320
|
+
chain = RankChain.for(diagram)
|
|
321
|
+
return chain_scene(diagram, chain, specifications) if chain
|
|
322
|
+
return noted_scene(diagram, specifications) if diagram.notes.any?
|
|
323
|
+
|
|
324
|
+
hierarchy = RankHierarchy.for(diagram)
|
|
325
|
+
if hierarchy
|
|
326
|
+
return hierarchy_scene(diagram, hierarchy, specifications)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
grid_scene(diagram, specifications)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def grid_scene(diagram, specifications)
|
|
333
|
+
box_width = widest_box(specifications)
|
|
334
|
+
boxes = position_boxes(rows_by_package(specifications, diagram),
|
|
335
|
+
box_width, stable_centers?(diagram),
|
|
336
|
+
intrinsic_box_widths?(diagram), diagram)
|
|
337
|
+
boxes = align_package_relation_targets(diagram, boxes,
|
|
338
|
+
specifications)
|
|
339
|
+
|
|
340
|
+
build_scene(diagram, boxes)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def box_specifications(diagram)
|
|
344
|
+
classes = diagram.classes.map do |klass|
|
|
345
|
+
box_specification(klass, diagram)
|
|
346
|
+
end
|
|
347
|
+
classes + note_specifications(diagram.notes)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def qualified_scene(diagram, specifications)
|
|
351
|
+
qualified = QualifiedAssociationLayout.for(diagram)
|
|
352
|
+
if qualified
|
|
353
|
+
qualified_association_scene(diagram, qualified, specifications)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def qualified_association_scene(diagram, qualified, specifications)
|
|
358
|
+
sizes = qualified.constrained_sizes(qualified_sizes(specifications))
|
|
359
|
+
specifications = resized_specifications(specifications, sizes)
|
|
360
|
+
corners = qualified.place(sizes, [MARGIN, MARGIN])
|
|
361
|
+
boxes = chain_boxes(specifications, corners, sizes)
|
|
362
|
+
width, height = chain_dimensions(boxes)
|
|
363
|
+
@qualified_layout_active = true
|
|
364
|
+
@qualified_route_middle = qualified_route_middle(boxes)
|
|
365
|
+
Scene.new(width: width, height: height, boxes: boxes, frames: [],
|
|
366
|
+
relations: scene_relations(diagram, boxes))
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def resized_specifications(specifications, sizes)
|
|
370
|
+
specifications.map do |item|
|
|
371
|
+
width, height = sizes.fetch(item[:id])
|
|
372
|
+
item.merge(width: width, height: height)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def qualified_sizes(specifications)
|
|
377
|
+
specifications.to_h do |item|
|
|
378
|
+
[item[:id], [item[:width], item[:height]]]
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def qualified_route_middle(boxes)
|
|
383
|
+
boxes.sum { |box| box.y + (box.height / 2) } / boxes.size
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def chain_scene(diagram, chain, specifications)
|
|
387
|
+
sizes = chain_sizes(specifications, diagram)
|
|
388
|
+
corners, nodes = chain.place(sizes, [MARGIN, MARGIN],
|
|
389
|
+
label_widths(diagram))
|
|
390
|
+
boxes = chain_boxes(specifications, corners, sizes)
|
|
391
|
+
width, height = chain_dimensions(boxes)
|
|
392
|
+
Scene.new(width: width, height: height,
|
|
393
|
+
boxes: boxes, frames: [],
|
|
394
|
+
relations: scene_relations(diagram, boxes, nodes))
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def hierarchy_scene(diagram, hierarchy, specifications)
|
|
398
|
+
items = specifications.reject { |item| item[:note] }
|
|
399
|
+
sizes = item_sizes(items)
|
|
400
|
+
corners = hierarchy.place(sizes, [MARGIN, MARGIN])
|
|
401
|
+
boxes = chain_boxes(items, corners, sizes)
|
|
402
|
+
width, height = chain_dimensions(boxes)
|
|
403
|
+
Scene.new(width: width, height: height, boxes: boxes, frames: [],
|
|
404
|
+
relations: scene_relations(diagram, boxes))
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def item_sizes(items)
|
|
408
|
+
items.to_h { |item| [item[:id], [item[:width], item[:height]]] }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Classes placed as without notes, then the notes around them; the
|
|
412
|
+
# whole drawing is moved so its top left corner keeps the margin.
|
|
413
|
+
def noted_scene(diagram, specifications)
|
|
414
|
+
items = specifications.reject { |item| item[:note] }
|
|
415
|
+
sizes, corners = noted_class_corners(diagram, items)
|
|
416
|
+
boxes = margined_boxes(diagram, items, sizes, corners)
|
|
417
|
+
width, height = chain_dimensions(boxes)
|
|
418
|
+
Scene.new(width: width, height: height, boxes: boxes, frames: [],
|
|
419
|
+
relations: scene_relations(diagram, boxes))
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# The class boxes and their notes, moved so the drawing's top left
|
|
423
|
+
# corner keeps the margin.
|
|
424
|
+
def margined_boxes(diagram, items, sizes, corners)
|
|
425
|
+
boxes = chain_boxes(items, corners, sizes)
|
|
426
|
+
shift = noted_shift(boxes + noted_boxes(diagram, boxes))
|
|
427
|
+
boxes = chain_boxes(items, shifted_corners(corners, shift), sizes)
|
|
428
|
+
boxes + noted_boxes(diagram, boxes)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def noted_class_corners(diagram, items)
|
|
432
|
+
hierarchy = RankHierarchy.for(diagram)
|
|
433
|
+
return hierarchy_corners(hierarchy, items) if hierarchy
|
|
434
|
+
|
|
435
|
+
grid_corners(diagram, items)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def hierarchy_corners(hierarchy, items)
|
|
439
|
+
sizes = item_sizes(items)
|
|
440
|
+
[sizes, hierarchy.place(sizes, [MARGIN, MARGIN])]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def grid_corners(diagram, items)
|
|
444
|
+
rows = rows_by_package(items, diagram)
|
|
445
|
+
placed = position_boxes(rows, widest_box(items),
|
|
446
|
+
stable_centers?(diagram),
|
|
447
|
+
intrinsic_box_widths?(diagram), diagram)
|
|
448
|
+
[placed.to_h { |box| [box.id, [box.width, box.height]] },
|
|
449
|
+
placed.to_h { |box| [box.id, [box.x, box.y]] }]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def noted_boxes(diagram, boxes)
|
|
453
|
+
NoteLayout.new(diagram, method(:note_line_width),
|
|
454
|
+
method(:member_text_width)).place(boxes)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def member_text_width(text)
|
|
458
|
+
standard_text_width(text, font_size)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def note_line_width(text)
|
|
462
|
+
measure_text(text, font_size: font_size * (13.0 / 14.0))[:width]
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def noted_shift(boxes)
|
|
466
|
+
[MARGIN - boxes.map(&:x).min, MARGIN - boxes.map(&:y).min]
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def shifted_corners(corners, shift)
|
|
470
|
+
corners.transform_values do |(left, top)|
|
|
471
|
+
[left + shift.first, top + shift.last]
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def chain_sizes(specifications, diagram)
|
|
476
|
+
widest = widest_box(specifications)
|
|
477
|
+
intrinsic_widths = intrinsic_box_widths?(diagram)
|
|
478
|
+
specifications.to_h do |item|
|
|
479
|
+
width = if compact_empty_item?(item, diagram) || intrinsic_widths
|
|
480
|
+
item[:width]
|
|
481
|
+
else
|
|
482
|
+
widest
|
|
483
|
+
end
|
|
484
|
+
[item[:id], [width, item[:height]]]
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def chain_dimensions(boxes)
|
|
489
|
+
[far_edge(boxes) { |box| box.x + box.width },
|
|
490
|
+
far_edge(boxes) { |box| box.y + box.height }]
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def chain_boxes(specifications, corners, sizes)
|
|
494
|
+
specifications.map do |item|
|
|
495
|
+
left, top = corners.fetch(item[:id])
|
|
496
|
+
build_box(item, left, top, sizes.fetch(item[:id]).first)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def member_note_family?(diagram)
|
|
501
|
+
return false unless diagram.classes.one? && diagram.notes.any?
|
|
502
|
+
return false unless standalone_diagram?(diagram)
|
|
503
|
+
|
|
504
|
+
klass = diagram.classes.first
|
|
505
|
+
plain_class?(klass) && member_notes?(diagram.notes, klass)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def class_note_pair_family?(diagram)
|
|
509
|
+
classes = diagram.classes
|
|
510
|
+
notes = diagram.notes
|
|
511
|
+
class_note_pair_counts?(classes, notes) &&
|
|
512
|
+
standalone_diagram?(diagram) &&
|
|
513
|
+
classes.all? { |klass| class_note_class?(klass) } &&
|
|
514
|
+
paired_class_notes?(classes, notes)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def class_note_pair_counts?(classes, notes)
|
|
518
|
+
classes.size > 1 && notes.size == classes.size
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def paired_class_notes?(classes, notes)
|
|
522
|
+
return false unless class_level_right_notes?(notes)
|
|
523
|
+
|
|
524
|
+
notes.map(&:target).sort == classes.map(&:name).sort
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def class_note_class?(klass)
|
|
528
|
+
plain_class?(klass) && klass.body.empty?
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def class_level_right_notes?(notes)
|
|
532
|
+
notes.all? { |note| note.member.nil? && note.side == :right }
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def standalone_diagram?(diagram)
|
|
536
|
+
diagram.relations.empty? && diagram.junctions.empty? &&
|
|
537
|
+
diagram.packages.empty?
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def plain_class?(klass)
|
|
541
|
+
klass.kind == :class && klass.stereotypes.empty? &&
|
|
542
|
+
klass.generics.nil?
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def member_notes?(notes, klass)
|
|
546
|
+
members = klass.body.map(&:name)
|
|
547
|
+
notes.all? do |note|
|
|
548
|
+
note.member && members.include?(note.member) &&
|
|
549
|
+
note.target == klass.name && note.side == :right
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def member_note_scene(diagram)
|
|
554
|
+
klass = diagram.classes.first
|
|
555
|
+
class_box = member_note_class_box(klass)
|
|
556
|
+
notes = member_note_boxes(diagram, klass, class_box)
|
|
557
|
+
boxes = [class_box, *notes]
|
|
558
|
+
boxes_only_scene(boxes)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def class_note_pair_scene(diagram)
|
|
562
|
+
specifications = diagram.classes.to_h do |klass|
|
|
563
|
+
[klass.name, box_specification(klass, diagram)]
|
|
564
|
+
end
|
|
565
|
+
notes = indexed_class_notes(diagram.notes)
|
|
566
|
+
boxes = place_class_note_pairs(diagram.classes, specifications,
|
|
567
|
+
notes)
|
|
568
|
+
boxes_only_scene(boxes)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def boxes_only_scene(boxes)
|
|
572
|
+
Scene.new(width: far_edge(boxes) { |box| box.x + box.width },
|
|
573
|
+
height: far_edge(boxes) { |box| box.y + box.height },
|
|
574
|
+
boxes: boxes, relations: [], frames: [])
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def indexed_class_notes(notes)
|
|
578
|
+
notes.each_with_index.to_h do |note, index|
|
|
579
|
+
[note.target, [note, index]]
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def place_class_note_pairs(classes, specifications, notes)
|
|
584
|
+
cursor = MARGIN
|
|
585
|
+
classes.flat_map do |klass|
|
|
586
|
+
item = specifications.fetch(klass.name)
|
|
587
|
+
class_box = build_box(item, cursor, MARGIN, item[:width])
|
|
588
|
+
note, index = notes.fetch(klass.name)
|
|
589
|
+
note_box = class_level_note_box(note, index, class_box)
|
|
590
|
+
cursor = note_box.x + note_box.width + CLASS_NOTE_GAP
|
|
591
|
+
[class_box, note_box]
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def class_level_note_box(note, index, class_box)
|
|
596
|
+
geometry = class_level_note_geometry(note, class_box)
|
|
597
|
+
Scene::Box.new(
|
|
598
|
+
id: "note-#{index}", **geometry,
|
|
599
|
+
texts: member_note_texts(note, geometry[:x], geometry[:y]),
|
|
600
|
+
separators: [class_level_note_pointer(class_box, geometry)],
|
|
601
|
+
kind: "member_note", fill: note.color
|
|
602
|
+
)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def class_level_note_geometry(note, class_box)
|
|
606
|
+
height = member_note_height(note)
|
|
607
|
+
{
|
|
608
|
+
x: class_box.x + class_box.width + CLASS_NOTE_GAP,
|
|
609
|
+
y: class_box.y + ((class_box.height - height) / 2),
|
|
610
|
+
width: class_level_note_width(note), height: height
|
|
611
|
+
}
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def class_level_note_pointer(class_box, geometry)
|
|
615
|
+
segment(class_box.x + class_box.width,
|
|
616
|
+
class_box.y + (class_box.height / 2),
|
|
617
|
+
geometry[:x], geometry[:y] + (geometry[:height] / 2))
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def class_level_note_width(note)
|
|
621
|
+
size = font_size * (13.0 / 14.0)
|
|
622
|
+
widths = note.lines.map do |line|
|
|
623
|
+
Drawing::Face.widest_line(line, size: size)
|
|
624
|
+
end
|
|
625
|
+
widths.max.to_f + 21.0
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def far_edge(boxes, &)
|
|
629
|
+
boxes.map(&).max + (MARGIN * 2)
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def member_note_class_box(klass)
|
|
633
|
+
width = member_note_class_width(klass)
|
|
634
|
+
height = MEMBER_NOTE_CLASS_HEIGHT +
|
|
635
|
+
(klass.body.size * MEMBER_NOTE_ROW_HEIGHT)
|
|
636
|
+
texts = member_note_class_texts(klass, width)
|
|
637
|
+
Scene::Box.new(
|
|
638
|
+
id: klass.name, x: MARGIN, y: MARGIN, width: width, height: height,
|
|
639
|
+
texts: texts, separators: [member_note_separator(width)],
|
|
640
|
+
kind: "class"
|
|
641
|
+
)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def member_note_separator(width)
|
|
645
|
+
separator_y = MARGIN + MEMBER_NOTE_CLASS_HEIGHT -
|
|
646
|
+
MEMBER_NOTE_ROW_HEIGHT
|
|
647
|
+
segment(MARGIN, separator_y, MARGIN + width, separator_y)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def member_note_class_width(klass)
|
|
651
|
+
# The pinned renderer's sans-serif is wider than the deterministic
|
|
652
|
+
# Arial table on this machine. This factor is measured across every
|
|
653
|
+
# member row in the reference-backed group2814 family.
|
|
654
|
+
name = plantuml_text_width(klass.name) + 32.0
|
|
655
|
+
members = klass.body.map do |member|
|
|
656
|
+
plantuml_text_width(member_text(member)) +
|
|
657
|
+
(MEMBER_NOTE_SIDE_PADDING * 2)
|
|
658
|
+
end
|
|
659
|
+
[name, *members].max
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def member_note_class_texts(klass, width)
|
|
663
|
+
title = scene_text(klass.name, MARGIN + (width / 2), MARGIN + 21.291,
|
|
664
|
+
"class_name", "middle")
|
|
665
|
+
members = klass.body.each_with_index.map do |member, index|
|
|
666
|
+
member_note_row_text(member, index)
|
|
667
|
+
end
|
|
668
|
+
[title, *members]
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def member_note_row_text(member, index)
|
|
672
|
+
vertical = MARGIN + 49.535 + (index * MEMBER_NOTE_ROW_HEIGHT)
|
|
673
|
+
role = member.type_position == :prefix ? "member_prefix" : "member"
|
|
674
|
+
scene_text(member_text(member), MARGIN + MEMBER_NOTE_SIDE_PADDING,
|
|
675
|
+
vertical, role, "start")
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def member_note_boxes(diagram, klass, class_box)
|
|
679
|
+
tops = member_note_tops(diagram.notes, class_box)
|
|
680
|
+
fill = shared_member_note_fill(diagram)
|
|
681
|
+
diagram.notes.each_with_index.map do |note, index|
|
|
682
|
+
box = member_note_box(note, index, klass, class_box, tops[index])
|
|
683
|
+
box.tap { |placed| placed.fill = fill }
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def member_note_tops(notes, class_box)
|
|
688
|
+
heights = notes.map { |note| member_note_height(note) }
|
|
689
|
+
first = first_note_top(class_box, heights)
|
|
690
|
+
heights.each_with_object([first]) do |height, tops|
|
|
691
|
+
tops << (tops.last + (height + MEMBER_NOTE_STACK_GAP))
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def first_note_top(class_box, heights)
|
|
696
|
+
total = heights.sum +
|
|
697
|
+
(MEMBER_NOTE_STACK_GAP * (heights.size - 1))
|
|
698
|
+
class_box.y + ((class_box.height - total) / 2) - 5.0
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def member_note_box(note, index, klass, class_box, top)
|
|
702
|
+
height = member_note_height(note)
|
|
703
|
+
left = class_box.x + class_box.width + MEMBER_NOTE_BODY_GAP
|
|
704
|
+
pointer = member_note_pointer(note, klass, class_box,
|
|
705
|
+
[left, top + (height / 2)])
|
|
706
|
+
Scene::Box.new(id: "note-#{index}", x: left, y: top,
|
|
707
|
+
width: member_note_width(note), height: height,
|
|
708
|
+
texts: member_note_texts(note, left, top),
|
|
709
|
+
separators: [pointer], kind: "member_note")
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def member_note_width(note)
|
|
713
|
+
note.lines.map { |line| plantuml_note_width(line) }.max.to_f + 21.0
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def member_note_pointer(note, klass, class_box, anchor)
|
|
717
|
+
target = member_note_target(note, klass, class_box)
|
|
718
|
+
segment(target[0], target[1], anchor[0], anchor[1])
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def member_note_target(note, klass, class_box)
|
|
722
|
+
index = klass.body.index { |member| member.name == note.member }
|
|
723
|
+
[member_note_target_x(klass.body.fetch(index), class_box),
|
|
724
|
+
class_box.y + 44.244 + (index * MEMBER_NOTE_ROW_HEIGHT)]
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def member_note_target_x(member, class_box)
|
|
728
|
+
class_box.x + MEMBER_NOTE_SIDE_PADDING +
|
|
729
|
+
plantuml_text_width(member_text(member))
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def member_note_texts(note, left, top)
|
|
733
|
+
note.lines.each_with_index.map do |line, index|
|
|
734
|
+
vertical = top + 17.568 + (index * MEMBER_NOTE_LINE_HEIGHT)
|
|
735
|
+
scene_text(line, left + 6.0, vertical, "member_note", "start")
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def member_note_height(note)
|
|
740
|
+
10.0 + ([note.lines.size, 1].max * MEMBER_NOTE_LINE_HEIGHT)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def shared_member_note_fill(diagram)
|
|
744
|
+
# PlantUML 1.2026.6 gives one side's shared tips entity the last
|
|
745
|
+
# note's paint. Preserve that pinned visible behavior for the stack.
|
|
746
|
+
note = diagram.notes.last
|
|
747
|
+
note.color || stereotype_note_fill(note.stereotype,
|
|
748
|
+
diagram.directives)
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def stereotype_note_fill(stereotype, directives)
|
|
752
|
+
return unless stereotype
|
|
753
|
+
|
|
754
|
+
name = stereotype.delete_prefix("<<").delete_suffix(">>")
|
|
755
|
+
prefix = "skinparam\\s+noteBackgroundColor<<#{Regexp.escape(name)}>>"
|
|
756
|
+
pattern = /\A#{prefix}\s+(\S+)\z/i
|
|
757
|
+
directive = directives.reverse.find { |line| line.match?(pattern) }
|
|
758
|
+
directive&.match(pattern)&.[](1)
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def plantuml_text_width(text)
|
|
762
|
+
measure_text(text, font_size: font_size)[:width] *
|
|
763
|
+
MEMBER_NOTE_TEXT_SCALE
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def plantuml_note_width(text)
|
|
767
|
+
measure_text(text, font_size: font_size * (13.0 / 14.0))[:width] *
|
|
768
|
+
MEMBER_NOTE_TEXT_SCALE
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def dress(diagram, scene)
|
|
772
|
+
measure = lambda do |text, size|
|
|
773
|
+
measure_text(text, font_size: size)[:width]
|
|
774
|
+
end
|
|
775
|
+
ClassHeader.apply(diagram, scene,
|
|
776
|
+
->(text) { measure.call(text, font_size) })
|
|
777
|
+
captions = CaptionLayout.new(diagram, normal_size: font_size,
|
|
778
|
+
measure: measure)
|
|
779
|
+
captions.dress(scene)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def build_scene(diagram, boxes)
|
|
783
|
+
frames = scene_frames(diagram, boxes)
|
|
784
|
+
width, height = scene_dimensions(diagram, boxes, frames)
|
|
785
|
+
Scene.new(width: width, height: height, boxes: boxes,
|
|
786
|
+
relations: scene_relations(diagram, boxes + frames),
|
|
787
|
+
frames: frames)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def scene_dimensions(diagram, boxes, frames)
|
|
791
|
+
unless package_endpoint_relation?(diagram)
|
|
792
|
+
return [canvas_width(diagram, boxes),
|
|
793
|
+
scene_height(diagram, boxes)]
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
figures = boxes + frames
|
|
797
|
+
[package_extent(figures, :x, :width),
|
|
798
|
+
package_extent(figures, :y, :height)]
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def package_extent(figures, origin, size)
|
|
802
|
+
figures.map do |item|
|
|
803
|
+
item.public_send(origin) + item.public_send(size)
|
|
804
|
+
end.max + PACKAGE_CANVAS_EDGE
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def scene_height(diagram, boxes)
|
|
808
|
+
room = PackageFrames::OPEN + PackageFrames::CLOSE
|
|
809
|
+
canvas_height(box_rows(boxes)) + (diagram.packages.size * room) +
|
|
810
|
+
trailing_margin(diagram)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def scene_frames(diagram, boxes)
|
|
814
|
+
PackageFrames.new(diagram, method(:measured_width)).call(boxes)
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# `nodes` holds each junction node as `[x, y, width, height]` by the
|
|
818
|
+
# index of its relation; without one the legs meet at the middle.
|
|
819
|
+
def scene_relations(diagram, boxes, nodes = {})
|
|
820
|
+
build_relations(diagram, boxes, nodes)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def box_specification(klass, diagram)
|
|
824
|
+
member_rows = klass.body.map { |member| member_text(member) }
|
|
825
|
+
names, generic = qualified_box_names(klass, diagram)
|
|
826
|
+
record = box_record(klass.name, header_rows(klass) + names,
|
|
827
|
+
member_rows)
|
|
828
|
+
details = { member_rows: member_rows, names: names, generic: generic }
|
|
829
|
+
complete_box_specification(record, klass, diagram, details)
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def qualified_box_names(klass, diagram)
|
|
833
|
+
qualified = !QualifiedAssociationLayout.for(diagram).nil?
|
|
834
|
+
names = name_rows(klass, separate_generics: qualified)
|
|
835
|
+
generic = klass.generics if qualified
|
|
836
|
+
[generic ? names[0...-1] : names, generic]
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def complete_box_specification(record, klass, diagram, details)
|
|
840
|
+
record = proportional_member_box(record, klass, diagram, details)
|
|
841
|
+
record = compartmented_empty_box(record, details[:member_rows],
|
|
842
|
+
diagram)
|
|
843
|
+
record = compact_empty_box(record, details[:member_rows], diagram)
|
|
844
|
+
record = smetana_hide_circle_box(record, klass, diagram, details)
|
|
845
|
+
adjust_plain_empty_width(record, klass, diagram, details[:names])
|
|
846
|
+
record = package_relation_box(record, klass, diagram, details)
|
|
847
|
+
record = multiline_title_box(record, klass, details)
|
|
848
|
+
record = isolated_box_sizes(record, klass, diagram, details)
|
|
849
|
+
with_box_context(record, klass, diagram, details)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def with_box_context(record, klass, diagram, details)
|
|
853
|
+
record.merge(name_rows: details[:names].size,
|
|
854
|
+
floating_generic: details[:generic],
|
|
855
|
+
**box_context(klass, diagram))
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# A box of one name row and some members is as wide as its widest
|
|
859
|
+
# proportional row and as tall as the reference row pitch makes it.
|
|
860
|
+
def proportional_member_box(record, klass, diagram, details)
|
|
861
|
+
rows = details[:member_rows]
|
|
862
|
+
return record if rows.empty? || record.fetch(:title_rows).size > 1
|
|
863
|
+
|
|
864
|
+
record.merge(
|
|
865
|
+
width: [*standard_title_widths(details[:names]),
|
|
866
|
+
*member_row_widths(klass, rows, diagram)].max,
|
|
867
|
+
height: pitched_member_height(klass, diagram, rows),
|
|
868
|
+
compact_members: true,
|
|
869
|
+
)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
MEMBER_PORTIONS = /\A(?:hide|show)\b.*\b(?:members|attributes|methods|
|
|
873
|
+
fields)\z/xi
|
|
874
|
+
private_constant :MEMBER_PORTIONS
|
|
875
|
+
|
|
876
|
+
# An empty field or method group keeps its 8-high compartment unless
|
|
877
|
+
# a `hide` or `show` line is in play.
|
|
878
|
+
def empty_gaps?(diagram)
|
|
879
|
+
diagram.directives.none? { |line| line.match?(MEMBER_PORTIONS) }
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def member_row_widths(klass, rows, diagram)
|
|
883
|
+
insets = member_insets_for(klass, diagram)
|
|
884
|
+
klass.body.each_index.filter_map do |index|
|
|
885
|
+
next unless insets[index]
|
|
886
|
+
|
|
887
|
+
member_row_width(rows[index], klass.body[index], insets[index])
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def member_insets_for(klass, diagram)
|
|
892
|
+
MemberIcon.insets(klass.body.map(&:kind),
|
|
893
|
+
klass.body.map(&:visibility),
|
|
894
|
+
MemberIcon.enabled?(diagram.directives))
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def member_row_width(row, member, inset)
|
|
898
|
+
text = MemberIcon.visible_text(row, member.visibility,
|
|
899
|
+
inset == MemberIcon::TEXT_INSET)
|
|
900
|
+
inset + standard_text_width(text, font_size) +
|
|
901
|
+
MemberIcon::TEXT_PLAIN
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def isolated_box_sizes(record, klass, diagram, details)
|
|
905
|
+
record = isolated_standard_box(record, klass, diagram, details)
|
|
906
|
+
record = isolated_class_kind_box(record, diagram, details)
|
|
907
|
+
record = hide_members_grid_box(record, diagram, details)
|
|
908
|
+
qualified_association_box(record, klass, diagram, details)
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def package_relation_box(record, klass, diagram, details)
|
|
912
|
+
return record unless package_endpoint_relation?(diagram)
|
|
913
|
+
return record unless plain_class?(klass) && klass.body.empty? &&
|
|
914
|
+
details[:names].one?
|
|
915
|
+
|
|
916
|
+
record.merge(width: standard_title_widths(details[:names]).max,
|
|
917
|
+
height: MEMBER_NOTE_CLASS_HEIGHT)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def qualified_association_box(record, klass, diagram, details)
|
|
921
|
+
return record unless QualifiedAssociationLayout.for(diagram)
|
|
922
|
+
|
|
923
|
+
record.merge(title_rows: details[:names],
|
|
924
|
+
width: qualified_box_width(klass, diagram, details),
|
|
925
|
+
height: qualified_height_with_members(
|
|
926
|
+
klass, diagram, details
|
|
927
|
+
),
|
|
928
|
+
compact_members: details[:member_rows].any?)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# A qualified box that carries members grows by their rows; its fixed
|
|
932
|
+
# height is only right for a box that is a bare title.
|
|
933
|
+
def qualified_height_with_members(klass, diagram, details)
|
|
934
|
+
height = qualified_box_height(diagram, details[:generic])
|
|
935
|
+
rows = details[:member_rows].size
|
|
936
|
+
return height if rows.zero?
|
|
937
|
+
|
|
938
|
+
base = qualified_compact?(diagram) ? COMPACT_RULE_BASE_HEIGHT : height
|
|
939
|
+
gaps = member_group_boundaries(klass.body.map(&:kind)).size
|
|
940
|
+
base + (rows * MEMBER_NOTE_ROW_HEIGHT) + (gaps * COMPACT_GROUP_GAP)
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# Indexes of the rows that open a method group after a field group.
|
|
944
|
+
def member_group_boundaries(kinds)
|
|
945
|
+
kinds.each_cons(2).with_index(1).filter_map do |(left, right), index|
|
|
946
|
+
index if left == :field && right == :method
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def qualified_box_width(klass, diagram, details)
|
|
951
|
+
widths = standard_title_widths(details[:names])
|
|
952
|
+
widths << qualified_generic_width(widths.max, details[:generic])
|
|
953
|
+
widths << qualified_incident_width(klass, diagram)
|
|
954
|
+
widths.concat(member_widths(details[:member_rows]))
|
|
955
|
+
widths.compact.max
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def member_widths(rows)
|
|
959
|
+
rows.map { |row| measured_width(row) + (BOX_PADDING * 2) }
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def qualified_generic_width(title_width, generic)
|
|
963
|
+
return unless generic
|
|
964
|
+
|
|
965
|
+
title_width + standard_text_width(generic,
|
|
966
|
+
QUALIFIED_GENERIC_FONT_SIZE) +
|
|
967
|
+
QUALIFIED_GENERIC_GAP
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def qualified_incident_width(klass, diagram)
|
|
971
|
+
text = incident_qualifiers(klass, diagram).max_by(&:length)
|
|
972
|
+
return unless text
|
|
973
|
+
|
|
974
|
+
standard_text_width(text, font_size) + qualifier_allowance(diagram)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def incident_qualifiers(klass, diagram)
|
|
978
|
+
diagram.relations.filter_map do |relation|
|
|
979
|
+
next unless relation.direction.nil?
|
|
980
|
+
next unless [relation.left, relation.right].include?(klass.name)
|
|
981
|
+
|
|
982
|
+
qualified_text_at(relation, klass.name)
|
|
983
|
+
end
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
def qualifier_allowance(diagram)
|
|
987
|
+
if qualified_compact?(diagram)
|
|
988
|
+
COMPACT_QUALIFIER_ALLOWANCE
|
|
989
|
+
else
|
|
990
|
+
QUALIFIED_QUALIFIER_ALLOWANCE
|
|
991
|
+
end
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def qualified_text_at(relation, name)
|
|
995
|
+
content = if relation.left == name
|
|
996
|
+
relation.left_multiplicity
|
|
997
|
+
else
|
|
998
|
+
relation.right_multiplicity
|
|
999
|
+
end
|
|
1000
|
+
return unless content&.start_with?("[")
|
|
1001
|
+
|
|
1002
|
+
content.delete_prefix("[").delete_suffix("]")
|
|
1003
|
+
.split("\\n").max_by(&:length)
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
def qualified_box_height(diagram, generic)
|
|
1007
|
+
return HIDDEN_MEMBER_EMPTY_HEIGHT if qualified_compact?(diagram)
|
|
1008
|
+
|
|
1009
|
+
generic ? QUALIFIED_GENERIC_HEIGHT : QUALIFIED_BOX_HEIGHT
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def qualified_compact?(diagram)
|
|
1013
|
+
diagram.directives.include?("hide empty members")
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
def hide_members_grid_box(record, diagram, details)
|
|
1017
|
+
return record unless hide_members_grid_family?(diagram)
|
|
1018
|
+
|
|
1019
|
+
members = details[:member_rows]
|
|
1020
|
+
widths = standard_title_widths(details[:names]) +
|
|
1021
|
+
isolated_class_kind_member_widths(members)
|
|
1022
|
+
record.merge(width: widths.max, compact_members: true,
|
|
1023
|
+
height: hide_members_box_height(members))
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def hide_members_box_height(members)
|
|
1027
|
+
return HIDDEN_MEMBER_EMPTY_HEIGHT if members.empty?
|
|
1028
|
+
|
|
1029
|
+
RESHOWN_MEMBER_BASE_HEIGHT +
|
|
1030
|
+
(members.size * MEMBER_NOTE_ROW_HEIGHT)
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
def isolated_class_kind_box(record, diagram, details)
|
|
1034
|
+
return record unless isolated_class_kind_family?(diagram)
|
|
1035
|
+
|
|
1036
|
+
widths = standard_title_widths(details[:names]) +
|
|
1037
|
+
isolated_class_kind_member_widths(details[:member_rows])
|
|
1038
|
+
record.merge(
|
|
1039
|
+
title_rows: details[:names], width: widths.max,
|
|
1040
|
+
compact_members: true,
|
|
1041
|
+
height: MEMBER_NOTE_CLASS_HEIGHT +
|
|
1042
|
+
(details[:member_rows].size * MEMBER_NOTE_ROW_HEIGHT)
|
|
1043
|
+
)
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
def isolated_class_kind_member_widths(rows)
|
|
1047
|
+
rows.map do |row|
|
|
1048
|
+
visible = row.sub(/\A[+\-#~]/, "")
|
|
1049
|
+
standard_text_width(visible, font_size) +
|
|
1050
|
+
ISOLATED_KIND_MEMBER_ALLOWANCE
|
|
1051
|
+
end
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
# A class with no members keeps its two 8-high compartments below
|
|
1055
|
+
# the name, which ClassHeader rules.
|
|
1056
|
+
def compartmented_empty_box(record, member_rows, diagram)
|
|
1057
|
+
return record unless member_rows.empty? && empty_gaps?(diagram)
|
|
1058
|
+
return record unless record.fetch(:title_rows).one?
|
|
1059
|
+
|
|
1060
|
+
record.merge(height: record.fetch(:height) + EMPTY_COMPARTMENTS_EXTRA)
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
def compact_empty_box(record, member_rows, diagram)
|
|
1064
|
+
return record unless member_rows.empty?
|
|
1065
|
+
return record unless compact_empty_members?(diagram)
|
|
1066
|
+
|
|
1067
|
+
record.merge(
|
|
1068
|
+
height: record[:height] - EMPTY_MEMBER_COMPARTMENT,
|
|
1069
|
+
width: record[:width] + EMPTY_CLASS_WIDTH_EXTRA,
|
|
1070
|
+
)
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def compact_empty_members?(diagram)
|
|
1074
|
+
RankChain.engine(diagram) == :smetana &&
|
|
1075
|
+
diagram.directives.include?("hide empty members")
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def compact_empty_item?(item, diagram)
|
|
1079
|
+
compact_empty_members?(diagram) && item[:member_rows]&.empty?
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def smetana_hide_circle_box(record, klass, diagram, details)
|
|
1083
|
+
return record unless smetana_hide_circle_class?(klass, diagram)
|
|
1084
|
+
|
|
1085
|
+
record.merge(
|
|
1086
|
+
width: hide_circle_box_width(details),
|
|
1087
|
+
compact_members: true,
|
|
1088
|
+
height: HIDE_CIRCLE_BASE_HEIGHT +
|
|
1089
|
+
(details[:member_rows].size * MEMBER_NOTE_ROW_HEIGHT),
|
|
1090
|
+
)
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def hide_circle_box_width(details)
|
|
1094
|
+
[
|
|
1095
|
+
*standard_row_widths(details[:names], font_size,
|
|
1096
|
+
HIDE_CIRCLE_TITLE_ALLOWANCE),
|
|
1097
|
+
*standard_row_widths(details[:member_rows], font_size,
|
|
1098
|
+
HIDE_CIRCLE_MEMBER_ALLOWANCE),
|
|
1099
|
+
].max
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
def smetana_hide_circle_class?(klass, diagram)
|
|
1103
|
+
RankChain.engine(diagram) == :smetana && plain_class?(klass) &&
|
|
1104
|
+
klass.package.nil? &&
|
|
1105
|
+
diagram.directives.any? { |line| line.match?(/\Ahide circle\z/i) }
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def adjust_plain_empty_width(specification, klass, diagram, names)
|
|
1109
|
+
if plain_empty_class?(klass, diagram, names)
|
|
1110
|
+
specification[:width] = plain_empty_class_width(names.first)
|
|
1111
|
+
elsif heterogeneous_smetana_empty_class?(klass, diagram, names)
|
|
1112
|
+
specification[:width] = standard_title_widths(names).max
|
|
1113
|
+
specification[:height] = MEMBER_NOTE_CLASS_HEIGHT
|
|
1114
|
+
end
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def plain_empty_class?(klass, diagram, names)
|
|
1118
|
+
return false unless simple_empty_class?(klass, names)
|
|
1119
|
+
return false unless uniform_empty_class_lengths?(diagram)
|
|
1120
|
+
|
|
1121
|
+
diagram.directives.none? do |line|
|
|
1122
|
+
line.match?(/\Ahide[ \t]+(?:circle|empty[ \t]+members)\z/i)
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
def simple_empty_class?(klass, names)
|
|
1127
|
+
plain_class?(klass) && klass.body.empty? && klass.package.nil? &&
|
|
1128
|
+
names.one?
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
def uniform_empty_class_lengths?(diagram)
|
|
1132
|
+
names = diagram.classes.map do |candidate|
|
|
1133
|
+
rows = name_rows(candidate)
|
|
1134
|
+
return false unless simple_empty_class?(candidate, rows)
|
|
1135
|
+
|
|
1136
|
+
rows.first
|
|
1137
|
+
end
|
|
1138
|
+
names.map(&:length).uniq.one?
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def heterogeneous_smetana_empty_class?(klass, diagram, names)
|
|
1142
|
+
simple_empty_class?(klass, names) &&
|
|
1143
|
+
heterogeneous_smetana_empty_classes?(diagram)
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
def heterogeneous_smetana_empty_classes?(diagram)
|
|
1147
|
+
return false unless plain_smetana_diagram?(diagram)
|
|
1148
|
+
|
|
1149
|
+
names = simple_empty_class_names(diagram)
|
|
1150
|
+
names && names.map(&:length).uniq.size > 1
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
def plain_smetana_diagram?(diagram)
|
|
1154
|
+
RankChain.engine(diagram) == :smetana &&
|
|
1155
|
+
!compact_empty_members?(diagram) &&
|
|
1156
|
+
diagram.directives.none? { |line| hide_or_show?(line) }
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
def simple_empty_class_names(diagram)
|
|
1160
|
+
diagram.classes.map do |candidate|
|
|
1161
|
+
rows = name_rows(candidate)
|
|
1162
|
+
return unless simple_empty_class?(candidate, rows)
|
|
1163
|
+
|
|
1164
|
+
rows.first
|
|
1165
|
+
end
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def hide_or_show?(line)
|
|
1169
|
+
line.match?(/\A(?:hide|show)\b/i)
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def plain_empty_class_width(name)
|
|
1173
|
+
title_width = Drawing::Face.widest_line(name, size: font_size)
|
|
1174
|
+
[((title_width * 2).round / 2.0) + 32.0,
|
|
1175
|
+
EMPTY_CLASS_MIN_WIDTH].max
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def multiline_title_box(record, klass, details)
|
|
1179
|
+
return record unless multiline_title_box?(record, klass, details)
|
|
1180
|
+
|
|
1181
|
+
rows = record.fetch(:title_rows).size
|
|
1182
|
+
height = MULTILINE_TITLE_VERTICAL_ALLOWANCE +
|
|
1183
|
+
(rows * MULTILINE_TITLE_ROW_HEIGHT)
|
|
1184
|
+
width = multiline_title_width(record.fetch(:title_rows))
|
|
1185
|
+
record.merge(width: width, height: height, compact_title_rows: true)
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
def multiline_title_box?(record, klass, details)
|
|
1189
|
+
details[:member_rows].empty? && record.fetch(:title_rows).size > 1 &&
|
|
1190
|
+
header_rows(klass).empty?
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
def multiline_title_width(rows)
|
|
1194
|
+
widths = rows.map do |row|
|
|
1195
|
+
measure_text(row, font_size: font_size)[:width]
|
|
1196
|
+
end
|
|
1197
|
+
widths.max.to_f + MULTILINE_TITLE_SIDE_ALLOWANCE
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
def isolated_standard_box(record, klass, diagram, details)
|
|
1201
|
+
return record unless isolated_standard_box?(klass, diagram, details)
|
|
1202
|
+
|
|
1203
|
+
record = record.merge(
|
|
1204
|
+
width: isolated_standard_width(klass, details),
|
|
1205
|
+
height: isolated_standard_height(klass, diagram,
|
|
1206
|
+
details[:member_rows]),
|
|
1207
|
+
compact_members: header_rows(klass).empty?,
|
|
1208
|
+
)
|
|
1209
|
+
stereotype_headed(record, klass, details)
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
# A lone class with one stereotype takes the jar's header: circle
|
|
1213
|
+
# at the left, stereotype above the name, rows packed from the foot.
|
|
1214
|
+
def stereotype_headed(record, klass, details)
|
|
1215
|
+
return record unless StereotypeHead.applies?(klass,
|
|
1216
|
+
circle: @kind_in_spot)
|
|
1217
|
+
|
|
1218
|
+
row = "«#{klass.stereotypes.first}»"
|
|
1219
|
+
sizes = stereotype_head_sizes(row, details[:names].first)
|
|
1220
|
+
width = [StereotypeHead.width(*sizes),
|
|
1221
|
+
*standard_member_widths(details[:member_rows])].max
|
|
1222
|
+
record.merge(width: width, compact_members: true,
|
|
1223
|
+
stereotype_head: { row: row, sizes: sizes })
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
def stereotype_head_sizes(row, name)
|
|
1227
|
+
[standard_text_width(row, font_size * 0.85),
|
|
1228
|
+
standard_text_width(name, font_size)]
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
def isolated_standard_box?(klass, diagram, details)
|
|
1232
|
+
lone_standard_class?(klass, diagram) &&
|
|
1233
|
+
(details[:member_rows].any? || klass.stereotypes.any?)
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
def lone_standard_class?(klass, diagram)
|
|
1237
|
+
diagram.classes.one? && diagram.notes.empty? &&
|
|
1238
|
+
standalone_diagram?(diagram) && klass.kind == :class &&
|
|
1239
|
+
klass.generics.nil?
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
def isolated_standard_width(klass, details)
|
|
1243
|
+
[standard_title_widths(details[:names]),
|
|
1244
|
+
standard_header_widths(klass),
|
|
1245
|
+
standard_member_widths(details[:member_rows])].flatten.max
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
def standard_title_widths(rows)
|
|
1249
|
+
standard_row_widths(rows, font_size,
|
|
1250
|
+
STANDARD_TITLE_SIDE_ALLOWANCE)
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
def standard_header_widths(klass)
|
|
1254
|
+
rows = header_rows(klass).map do |row|
|
|
1255
|
+
row.gsub(/<<([^<>]+)>>/, '«\1»')
|
|
1256
|
+
end
|
|
1257
|
+
standard_row_widths(rows, font_size * (12.0 / 14.0),
|
|
1258
|
+
STANDARD_TITLE_SIDE_ALLOWANCE)
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
def standard_member_widths(rows)
|
|
1262
|
+
standard_row_widths(rows, font_size,
|
|
1263
|
+
STANDARD_MEMBER_SIDE_ALLOWANCE)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
def standard_row_widths(rows, size, allowance)
|
|
1267
|
+
rows.map { |row| standard_text_width(row, size) + allowance }
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
def standard_text_width(text, size)
|
|
1271
|
+
Drawing::Face.widest_line(text, size: size)
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
def isolated_standard_height(klass, diagram, member_rows)
|
|
1275
|
+
headers = header_rows(klass)
|
|
1276
|
+
unless headers.empty?
|
|
1277
|
+
return MEMBER_NOTE_CLASS_HEIGHT +
|
|
1278
|
+
(member_rows.size * MEMBER_NOTE_ROW_HEIGHT) +
|
|
1279
|
+
(headers.size * STANDARD_STEREOTYPE_ROW_HEIGHT)
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
pitched_member_height(klass, diagram, member_rows)
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
def pitched_member_height(klass, diagram, member_rows)
|
|
1286
|
+
PROPORTIONAL_MEMBER_BASE_HEIGHT +
|
|
1287
|
+
MemberPitch.layout(klass.body.map(&:kind), member_rows,
|
|
1288
|
+
gaps: empty_gaps?(diagram)).last
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
def box_context(klass, diagram)
|
|
1292
|
+
modifiers = klass.body.map { |member| rule_modifiers(member) }
|
|
1293
|
+
{ member_modifiers: modifiers, package: klass.package,
|
|
1294
|
+
member_kinds: klass.body.map(&:kind),
|
|
1295
|
+
member_visibilities: klass.body.map(&:visibility),
|
|
1296
|
+
icons_enabled: MemberIcon.enabled?(diagram.directives),
|
|
1297
|
+
empty_gaps: empty_gaps?(diagram),
|
|
1298
|
+
chain: diagram.package_chain(klass.package) }
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
# A separator row carries the pseudo-modifier :rule, which the box
|
|
1302
|
+
# draws as a line instead of text.
|
|
1303
|
+
def rule_modifiers(member)
|
|
1304
|
+
member.kind == :separator ? [:rule, member.type] : member.modifiers
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def note_specifications(notes)
|
|
1308
|
+
notes.each_with_index.map do |note, index|
|
|
1309
|
+
rows = note.lines
|
|
1310
|
+
{ id: "note-#{index}", note: note, rows: rows,
|
|
1311
|
+
width: measured_box_width(rows),
|
|
1312
|
+
height: (BOX_PADDING * 2) + ([rows.size, 1].max * ROW_HEIGHT) }
|
|
1313
|
+
end
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
def box_record(name, title_rows, member_rows)
|
|
1317
|
+
rows = title_rows + member_rows
|
|
1318
|
+
|
|
1319
|
+
specification = { id: name }
|
|
1320
|
+
specification[:width] = measured_box_width(rows)
|
|
1321
|
+
specification[:height] = box_height(rows, member_rows)
|
|
1322
|
+
specification[:title_rows] = title_rows
|
|
1323
|
+
specification[:member_rows] = member_rows
|
|
1324
|
+
specification
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
def measured_box_width(rows)
|
|
1328
|
+
measured = rows.map { |text| measured_width(text) }
|
|
1329
|
+
measured.max.to_f + (BOX_PADDING * 2)
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
def box_height(rows, member_rows)
|
|
1333
|
+
height = (BOX_PADDING * 2) + (rows.size * ROW_HEIGHT)
|
|
1334
|
+
member_rows.empty? ? height : height + 6.0
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
def header_rows(klass)
|
|
1338
|
+
kind = ["<<#{klass.kind}>>"] unless spot_names_kind?(klass)
|
|
1339
|
+
tags = klass.stereotypes.map { |tag| "<<#{tag}>>" }
|
|
1340
|
+
[*kind, *(tags.join(" ") unless tags.empty?)]
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# A class whose one-line name carries a circle needs no `<<kind>>`
|
|
1344
|
+
# row.
|
|
1345
|
+
def spot_names_kind?(klass)
|
|
1346
|
+
klass.kind == :class ||
|
|
1347
|
+
(@kind_in_spot && !ClassHeader.glyph(klass.kind).nil? &&
|
|
1348
|
+
ClassName.lines(klass.name).one?)
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def name_rows(klass, separate_generics: false)
|
|
1352
|
+
*first, last = ClassName.lines(klass.name)
|
|
1353
|
+
return [*first, last, klass.generics] if separate_generics &&
|
|
1354
|
+
klass.generics
|
|
1355
|
+
|
|
1356
|
+
[*first, (klass.generics ? "#{last}<#{klass.generics}>" : last)]
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
def measured_width(text)
|
|
1360
|
+
measure_text(text, font_size: font_size, monospace: true)[:width]
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
def font_size
|
|
1364
|
+
theme.typography.font_size_normal.to_f
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
def member_text(member)
|
|
1368
|
+
return member.name if member.kind == :separator
|
|
1369
|
+
|
|
1370
|
+
name = member.name.dup
|
|
1371
|
+
name += "(#{member.parameters})" if member.parameters
|
|
1372
|
+
type = member.type
|
|
1373
|
+
content = if type && member.type_position == :prefix
|
|
1374
|
+
"#{type} #{name}"
|
|
1375
|
+
else
|
|
1376
|
+
"#{name}#{"#{member.type_separator}#{type}" if type}"
|
|
1377
|
+
end
|
|
1378
|
+
"#{visibility_mark(member.visibility)}#{content}"
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
def visibility_mark(visibility)
|
|
1382
|
+
MemberIcon.mark(visibility)
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
# Classes outside any package first, then each package's own classes
|
|
1386
|
+
# in the order the packages open (an outer package before the ones
|
|
1387
|
+
# inside it), every group padded to whole rows so a row holds one
|
|
1388
|
+
# group only.
|
|
1389
|
+
def rows_by_package(specifications, diagram)
|
|
1390
|
+
columns = column_count(specifications)
|
|
1391
|
+
package_groups(specifications, diagram).flat_map do |group|
|
|
1392
|
+
group.each_slice(columns).map { |row| pad(row, columns) }
|
|
1393
|
+
end
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
def package_groups(specifications, diagram)
|
|
1397
|
+
by_package = specifications.group_by { |item| item[:package] }
|
|
1398
|
+
ids = package_group_ids(diagram)
|
|
1399
|
+
ids.filter_map { |id| by_package[id] }
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
def package_group_ids(diagram)
|
|
1403
|
+
package_ids = diagram.packages.map(&:id)
|
|
1404
|
+
source_package = diagram.relations.any? do |relation|
|
|
1405
|
+
package_ids.include?(relation.left) &&
|
|
1406
|
+
!package_ids.include?(relation.right)
|
|
1407
|
+
end
|
|
1408
|
+
source_package ? [*package_ids, nil] : [nil, *package_ids]
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def pad(row, columns)
|
|
1412
|
+
row + Array.new(columns - row.size)
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1415
|
+
def stable_centers?(diagram)
|
|
1416
|
+
horizontal = diagram.directives.include?("left to right direction")
|
|
1417
|
+
diagram.relations.any? do |relation|
|
|
1418
|
+
qualifiers = [relation.left_multiplicity,
|
|
1419
|
+
relation.right_multiplicity]
|
|
1420
|
+
# PlantUML reserves relation-label lanes for qualifiers and for
|
|
1421
|
+
# fully annotated horizontal ends independently of class chrome.
|
|
1422
|
+
qualifiers.any? { |text| text&.start_with?("[") } ||
|
|
1423
|
+
(horizontal && annotated_ends?(relation))
|
|
1424
|
+
end
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
def annotated_ends?(relation)
|
|
1428
|
+
[relation.left_role, relation.right_role].compact.size == 2
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
def intrinsic_box_widths?(diagram)
|
|
1432
|
+
package_endpoint_relation?(diagram) || intrinsic_content?(diagram)
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
def intrinsic_content?(diagram)
|
|
1436
|
+
body = diagram.classes.any? { |klass| klass.body.any? }
|
|
1437
|
+
diagram.notes.any? || body ||
|
|
1438
|
+
heterogeneous_smetana_empty_classes?(diagram) ||
|
|
1439
|
+
diagram.directives.any? { |line| hide_or_show?(line) }
|
|
1440
|
+
end
|
|
1441
|
+
|
|
1442
|
+
def position_boxes(rows, box_width, stable_centers, intrinsic_widths,
|
|
1443
|
+
diagram)
|
|
1444
|
+
tops = row_tops(rows, diagram)
|
|
1445
|
+
placement = box_placement(rows, box_width, stable_centers,
|
|
1446
|
+
intrinsic_widths, diagram)
|
|
1447
|
+
rows.each_with_index.flat_map do |row, row_index|
|
|
1448
|
+
position_row(row, tops[row_index], box_width, placement)
|
|
1449
|
+
end
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
def box_placement(rows, box_width, stable_centers, intrinsic_widths,
|
|
1453
|
+
diagram)
|
|
1454
|
+
{ left: left_margin(rows, diagram),
|
|
1455
|
+
footprint: box_footprint(box_width, stable_centers),
|
|
1456
|
+
box_width: box_width,
|
|
1457
|
+
intrinsic_widths: intrinsic_widths,
|
|
1458
|
+
left_align: intrinsic_widths &&
|
|
1459
|
+
package_endpoint_relation?(diagram) }
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
def position_row(row, top, box_width, placement)
|
|
1463
|
+
row.each_with_index.filter_map do |item, column|
|
|
1464
|
+
x = box_left(item, column, placement)
|
|
1465
|
+
width = positioned_box_width(item, box_width, placement)
|
|
1466
|
+
build_box(item, x, top, width) if item
|
|
1467
|
+
end
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
def positioned_box_width(item, box_width, placement)
|
|
1471
|
+
return box_width unless placement[:intrinsic_widths]
|
|
1472
|
+
|
|
1473
|
+
item&.fetch(:width)
|
|
1474
|
+
end
|
|
1475
|
+
|
|
1476
|
+
def box_footprint(box_width, stable_centers)
|
|
1477
|
+
return box_width unless stable_centers
|
|
1478
|
+
|
|
1479
|
+
[ANNOTATED_RELATION_SLOT_WIDTH, box_width].max
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
def box_left(item, column, placement)
|
|
1483
|
+
slot = placement[:left] +
|
|
1484
|
+
(column * (placement[:footprint] + COLUMN_GAP))
|
|
1485
|
+
return slot if placement[:left_align]
|
|
1486
|
+
|
|
1487
|
+
width = if item && placement[:intrinsic_widths]
|
|
1488
|
+
item.fetch(:width)
|
|
1489
|
+
else
|
|
1490
|
+
placement[:box_width]
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
slot + ((placement[:footprint] - width) / 2)
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1496
|
+
def row_tops(rows, diagram)
|
|
1497
|
+
chains = rows.map { |row| row.compact.first[:chain] || [] }
|
|
1498
|
+
first = [top_margin(diagram) +
|
|
1499
|
+
frame_space([], chains.first, diagram)]
|
|
1500
|
+
steps = row_steps(rows, chains, diagram)
|
|
1501
|
+
steps.each_with_object(first) do |step, tops|
|
|
1502
|
+
tops << (tops.last + step)
|
|
1503
|
+
end
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
def row_steps(rows, chains, diagram)
|
|
1507
|
+
row_heights(rows).each_with_index.map do |height, index|
|
|
1508
|
+
below = chains[index + 1] || []
|
|
1509
|
+
space = frame_space(chains[index], below, diagram)
|
|
1510
|
+
height + row_gap(diagram, chains[index], below) + space
|
|
1511
|
+
end
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def row_heights(rows)
|
|
1515
|
+
rows.map { |row| row.compact.map { |item| item[:height] }.max }
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
# The room for the frames closed on leaving the packages `above` and
|
|
1519
|
+
# opened on entering the packages `below`, each given outermost first.
|
|
1520
|
+
def frame_space(above, below, diagram)
|
|
1521
|
+
shared = above.zip(below).take_while { |a, b| a == b }.size
|
|
1522
|
+
closing = above.size - shared
|
|
1523
|
+
opening = below.size - shared
|
|
1524
|
+
if package_endpoint_relation?(diagram)
|
|
1525
|
+
return nested_frame_space(closing, opening)
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1528
|
+
(closing * PackageFrames::CLOSE) +
|
|
1529
|
+
(opening * PackageFrames::OPEN)
|
|
1530
|
+
end
|
|
1531
|
+
|
|
1532
|
+
def nested_frame_space(closing, opening)
|
|
1533
|
+
PackageFrames.stack_close(closing) +
|
|
1534
|
+
PackageFrames.stack_open(opening)
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
def top_margin(diagram)
|
|
1538
|
+
package_endpoint_relation?(diagram) ? PACKAGE_CANVAS_MARGIN : MARGIN
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
def row_gap(diagram, above, below)
|
|
1542
|
+
crossing = above.empty? != below.empty?
|
|
1543
|
+
if package_endpoint_relation?(diagram) && crossing
|
|
1544
|
+
PACKAGE_OUTER_GAP
|
|
1545
|
+
else
|
|
1546
|
+
ROW_GAP
|
|
1547
|
+
end
|
|
1548
|
+
end
|
|
1549
|
+
|
|
1550
|
+
def column_count(items)
|
|
1551
|
+
items.compact.size > 1 ? 2 : 1
|
|
1552
|
+
end
|
|
1553
|
+
|
|
1554
|
+
# Room on the left for the deepest stack of package frames.
|
|
1555
|
+
def left_margin(rows, diagram)
|
|
1556
|
+
items = rows.flat_map(&:compact)
|
|
1557
|
+
depth = package_depth(items)
|
|
1558
|
+
if package_endpoint_relation?(diagram)
|
|
1559
|
+
return package_left_margin(depth)
|
|
1560
|
+
end
|
|
1561
|
+
|
|
1562
|
+
return MARGIN if depth.zero?
|
|
1563
|
+
|
|
1564
|
+
[MARGIN, (depth * PackageFrames::SIDE) + 8.0].max
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
def package_depth(items)
|
|
1568
|
+
items.filter_map { |item| item[:chain]&.size }.max.to_i
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def package_left_margin(depth)
|
|
1572
|
+
PACKAGE_CANVAS_MARGIN + PackageFrames.stack_side(depth)
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
def package_endpoint_relation?(diagram)
|
|
1576
|
+
PackageFrames.relation_layout?(diagram)
|
|
1577
|
+
end
|
|
1578
|
+
|
|
1579
|
+
def align_package_relation_targets(diagram, boxes, specifications)
|
|
1580
|
+
by_name = boxes.to_h { |box| [box.id, box] }
|
|
1581
|
+
specs = specifications.to_h { |item| [item[:id], item] }
|
|
1582
|
+
replace_package_targets(diagram, by_name, specs)
|
|
1583
|
+
boxes.map { |box| by_name.fetch(box.id) }
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
def replace_package_targets(diagram, by_name, specs)
|
|
1587
|
+
diagram.relations.each do |relation|
|
|
1588
|
+
replacement = aligned_package_target(diagram, by_name, specs,
|
|
1589
|
+
relation)
|
|
1590
|
+
by_name[replacement.id] = replacement if replacement
|
|
1591
|
+
end
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
def aligned_package_target(diagram, by_name, specs, relation)
|
|
1595
|
+
return unless package_source?(diagram, relation)
|
|
1596
|
+
|
|
1597
|
+
target = by_name[relation.right]
|
|
1598
|
+
descendants = package_descendant_boxes(diagram, by_name,
|
|
1599
|
+
relation.left)
|
|
1600
|
+
return unless target && descendants.any?
|
|
1601
|
+
|
|
1602
|
+
build_aligned_package_target(diagram, specs, relation, target,
|
|
1603
|
+
descendants)
|
|
1604
|
+
end
|
|
1605
|
+
|
|
1606
|
+
def build_aligned_package_target(diagram, specs, relation, target,
|
|
1607
|
+
descendants)
|
|
1608
|
+
left = package_target_left(diagram, descendants, target, relation)
|
|
1609
|
+
build_box(specs.fetch(target.id), left, target.y, target.width)
|
|
1610
|
+
end
|
|
1611
|
+
|
|
1612
|
+
def package_source?(diagram, relation)
|
|
1613
|
+
diagram.packages.any? { |item| item.id == relation.left }
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
def package_target_left(diagram, descendants, target, relation)
|
|
1617
|
+
edge = descendants.map { |box| box.x + box.width }.max
|
|
1618
|
+
depth = package_relation_depth(diagram, relation.left)
|
|
1619
|
+
edge + PackageFrames.stack_side(depth) - (target.width / 2)
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
def package_descendant_boxes(diagram, by_name, package_id)
|
|
1623
|
+
diagram.classes.filter_map do |klass|
|
|
1624
|
+
chain = diagram.package_chain(klass.package)
|
|
1625
|
+
next unless chain.include?(package_id)
|
|
1626
|
+
|
|
1627
|
+
by_name[klass.name]
|
|
1628
|
+
end
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
def package_relation_depth(diagram, package_id)
|
|
1632
|
+
diagram.classes.filter_map do |klass|
|
|
1633
|
+
chain = diagram.package_chain(klass.package)
|
|
1634
|
+
index = chain.index(package_id)
|
|
1635
|
+
chain.size - index if index
|
|
1636
|
+
end.max
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
def build_box(item, left, top, box_width)
|
|
1640
|
+
texts, separators = box_contents(item, left, top, box_width)
|
|
1641
|
+
|
|
1642
|
+
Scene::Box.new(
|
|
1643
|
+
id: item[:id], x: left, y: top, width: box_width,
|
|
1644
|
+
height: item[:height], texts: texts, separators: separators,
|
|
1645
|
+
kind: item[:note] ? "note" : "class", fill: item[:note]&.color,
|
|
1646
|
+
spot: stereotype_spot(item, left, top)
|
|
1647
|
+
)
|
|
1648
|
+
end
|
|
1649
|
+
|
|
1650
|
+
def stereotype_spot(item, left, top)
|
|
1651
|
+
return unless item[:stereotype_head]
|
|
1652
|
+
|
|
1653
|
+
across, down = StereotypeHead.circle_centre
|
|
1654
|
+
Scene::Spot.new(kind: "class", cx: left + across, cy: top + down)
|
|
1655
|
+
end
|
|
1656
|
+
|
|
1657
|
+
def note_texts(item, horizontal, vertical)
|
|
1658
|
+
item[:rows].each_with_index.map do |content, index|
|
|
1659
|
+
baseline = vertical + BOX_PADDING + font_size + (index * ROW_HEIGHT)
|
|
1660
|
+
scene_text(content, horizontal + BOX_PADDING, baseline,
|
|
1661
|
+
"note", "start")
|
|
1662
|
+
end
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
def box_contents(item, horizontal, vertical, width)
|
|
1666
|
+
return [note_texts(item, horizontal, vertical), []] if item[:note]
|
|
1667
|
+
|
|
1668
|
+
item = item.merge(compact_bottom: vertical + item[:height])
|
|
1669
|
+
texts, cursor = title_texts(item, horizontal, vertical, width)
|
|
1670
|
+
members, separators = member_contents(
|
|
1671
|
+
item, horizontal, cursor, width
|
|
1672
|
+
)
|
|
1673
|
+
[texts + members, separators]
|
|
1674
|
+
end
|
|
1675
|
+
|
|
1676
|
+
def title_texts(item, horizontal, vertical, width)
|
|
1677
|
+
if item[:stereotype_head]
|
|
1678
|
+
return stereotype_head_texts(item, horizontal, vertical)
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
cursor = title_text_cursor(item, vertical)
|
|
1682
|
+
texts, cursor = title_text_rows(item, horizontal, width, cursor)
|
|
1683
|
+
texts << floating_generic_text(item, horizontal, vertical, width)
|
|
1684
|
+
[texts.compact, cursor]
|
|
1685
|
+
end
|
|
1686
|
+
|
|
1687
|
+
def stereotype_head_texts(item, horizontal, vertical)
|
|
1688
|
+
head = item[:stereotype_head]
|
|
1689
|
+
centre = horizontal + StereotypeHead.centre(*head[:sizes])
|
|
1690
|
+
first, second = StereotypeHead.baselines
|
|
1691
|
+
[[scene_text(head[:row], centre, vertical + first, "kind", "middle"),
|
|
1692
|
+
scene_text(item[:title_rows].last, centre, vertical + second,
|
|
1693
|
+
"class_name", "middle")], vertical + second]
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1696
|
+
def title_text_rows(item, horizontal, width, cursor)
|
|
1697
|
+
texts = item[:title_rows].each_with_index.map do |content, index|
|
|
1698
|
+
text = scene_text(content, horizontal + (width / 2), cursor,
|
|
1699
|
+
title_role(item, index), "middle")
|
|
1700
|
+
cursor += title_row_height(item)
|
|
1701
|
+
text
|
|
1702
|
+
end
|
|
1703
|
+
[texts, cursor]
|
|
1704
|
+
end
|
|
1705
|
+
|
|
1706
|
+
def title_text_cursor(item, vertical)
|
|
1707
|
+
if item[:compact_title_rows]
|
|
1708
|
+
return vertical + MULTILINE_TITLE_BASELINE
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
baseline = vertical + BOX_PADDING + font_size
|
|
1712
|
+
return baseline unless compact_single_title?(item)
|
|
1713
|
+
|
|
1714
|
+
[baseline, compact_row_geometry(item)[:head] - 10.709].min
|
|
1715
|
+
end
|
|
1716
|
+
|
|
1717
|
+
def compact_single_title?(item)
|
|
1718
|
+
item[:compact_members] && item[:title_rows].size == 1 &&
|
|
1719
|
+
item[:member_rows].any?
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
def title_row_height(item)
|
|
1723
|
+
item[:compact_title_rows] ? MULTILINE_TITLE_ROW_HEIGHT : ROW_HEIGHT
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
def floating_generic_text(item, horizontal, vertical, width)
|
|
1727
|
+
generic = item[:floating_generic]
|
|
1728
|
+
return unless generic
|
|
1729
|
+
|
|
1730
|
+
scaled_width = measured_width(generic) * 0.85
|
|
1731
|
+
scene_text(generic, horizontal + width - scaled_width + 4.0,
|
|
1732
|
+
vertical + (font_size * 0.9), "kind", "start")
|
|
1733
|
+
end
|
|
1734
|
+
|
|
1735
|
+
def title_role(item, index)
|
|
1736
|
+
header = item[:title_rows].size - item[:name_rows]
|
|
1737
|
+
index < header ? "kind" : "class_name"
|
|
1738
|
+
end
|
|
1739
|
+
|
|
1740
|
+
def member_contents(item, horizontal, cursor, width)
|
|
1741
|
+
return [[], []] if item[:member_rows].empty?
|
|
1742
|
+
|
|
1743
|
+
rows = member_row_geometry(item, cursor)
|
|
1744
|
+
[member_texts(item, horizontal, width, rows[:baselines]),
|
|
1745
|
+
member_lines(item, horizontal, width, rows)]
|
|
1746
|
+
end
|
|
1747
|
+
|
|
1748
|
+
def member_lines(item, horizontal, width, rows)
|
|
1749
|
+
base = rows[:baselines]
|
|
1750
|
+
[segment(horizontal, rows[:head], horizontal + width, rows[:head]),
|
|
1751
|
+
*group_rules(item, horizontal, width, base),
|
|
1752
|
+
*underlines(item, horizontal, base),
|
|
1753
|
+
*rules(item, horizontal, width, base)]
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
# Baselines of the member rows and the y of the rule above them. The
|
|
1757
|
+
# default box stacks rows from the title; a compact box, whose height
|
|
1758
|
+
# was sized from the reference row pitch, stacks them up from its
|
|
1759
|
+
# bottom edge so the last row always lies inside it.
|
|
1760
|
+
def member_row_geometry(item, cursor)
|
|
1761
|
+
return compact_row_geometry(item) if item[:compact_members]
|
|
1762
|
+
|
|
1763
|
+
{ head: cursor - (ROW_HEIGHT / 2),
|
|
1764
|
+
baselines: item[:member_rows].each_index.map do |index|
|
|
1765
|
+
cursor + 6.0 + (index * ROW_HEIGHT)
|
|
1766
|
+
end }
|
|
1767
|
+
end
|
|
1768
|
+
|
|
1769
|
+
def compact_row_geometry(item)
|
|
1770
|
+
offsets = compact_row_offsets(item)
|
|
1771
|
+
last = item.fetch(:compact_bottom) - COMPACT_BOTTOM_PAD -
|
|
1772
|
+
empty_gap(item, :tail)
|
|
1773
|
+
baselines = offsets.map { |offset| last - offsets.last + offset }
|
|
1774
|
+
{ head: baselines.first - COMPACT_HEAD_GAP - offsets.first,
|
|
1775
|
+
baselines: baselines }
|
|
1776
|
+
end
|
|
1777
|
+
|
|
1778
|
+
# `:lead` or `:tail`: the gap an empty field or method group keeps.
|
|
1779
|
+
def empty_gap(item, side)
|
|
1780
|
+
return 0.0 unless item[:empty_gaps]
|
|
1781
|
+
|
|
1782
|
+
MemberPitch.public_send(side, item[:member_kinds])
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1785
|
+
def compact_row_offsets(item)
|
|
1786
|
+
MemberPitch.layout(item[:member_kinds], item[:member_rows],
|
|
1787
|
+
gaps: item[:empty_gaps]).first
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
# A thin rule between the field group and the method group, and one
|
|
1791
|
+
# closing whichever of them is empty.
|
|
1792
|
+
def group_rules(item, horizontal, width, baselines)
|
|
1793
|
+
return [] unless item[:compact_members]
|
|
1794
|
+
|
|
1795
|
+
group_rule_heights(item, baselines).map do |at|
|
|
1796
|
+
segment(horizontal, at, horizontal + width, at)
|
|
1797
|
+
end
|
|
1798
|
+
end
|
|
1799
|
+
|
|
1800
|
+
def group_rule_heights(item, baselines)
|
|
1801
|
+
member_group_boundaries(item[:member_kinds]).map do |index|
|
|
1802
|
+
((baselines[index - 1] + baselines[index]) / 2.0) - 5.2911
|
|
1803
|
+
end + empty_group_rules(item, baselines)
|
|
1804
|
+
end
|
|
1805
|
+
|
|
1806
|
+
def empty_group_rules(item, baselines)
|
|
1807
|
+
[(baselines.first - COMPACT_HEAD_GAP if
|
|
1808
|
+
empty_gap(item, :lead).positive?),
|
|
1809
|
+
(baselines.last + COMPACT_BOTTOM_PAD if
|
|
1810
|
+
empty_gap(item, :tail).positive?)].compact
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def member_texts(item, horizontal, width, baselines)
|
|
1814
|
+
insets = member_insets(item)
|
|
1815
|
+
item[:member_rows].each_with_index.filter_map do |content, index|
|
|
1816
|
+
next if content.empty?
|
|
1817
|
+
|
|
1818
|
+
text = member_text_row(visible_member(item, content, index),
|
|
1819
|
+
item[:member_modifiers][index],
|
|
1820
|
+
[horizontal, width, insets[index]],
|
|
1821
|
+
baselines.fetch(index))
|
|
1822
|
+
text.icon = member_icon(item, index, horizontal, baselines)
|
|
1823
|
+
text
|
|
1824
|
+
end
|
|
1825
|
+
end
|
|
1826
|
+
|
|
1827
|
+
def member_icon(item, index, horizontal, baselines)
|
|
1828
|
+
return unless item[:icons_enabled]
|
|
1829
|
+
|
|
1830
|
+
MemberIcon.at(item[:member_visibilities][index],
|
|
1831
|
+
item[:member_kinds][index], horizontal,
|
|
1832
|
+
baselines.fetch(index))
|
|
1833
|
+
end
|
|
1834
|
+
|
|
1835
|
+
def member_insets(item)
|
|
1836
|
+
MemberIcon.insets(item[:member_kinds], item[:member_visibilities],
|
|
1837
|
+
item[:icons_enabled])
|
|
1838
|
+
end
|
|
1839
|
+
|
|
1840
|
+
def visible_member(item, content, index)
|
|
1841
|
+
MemberIcon.visible_text(content, item[:member_visibilities][index],
|
|
1842
|
+
item[:icons_enabled])
|
|
1843
|
+
end
|
|
1844
|
+
|
|
1845
|
+
# A separator's title is centred; any other row starts at its inset.
|
|
1846
|
+
def member_text_row(content, modifiers, box, vertical)
|
|
1847
|
+
left, width, inset = box
|
|
1848
|
+
if modifiers.include?(:rule)
|
|
1849
|
+
return scene_text(content, left + (width / 2.0), vertical,
|
|
1850
|
+
"member", "middle")
|
|
1851
|
+
end
|
|
1852
|
+
|
|
1853
|
+
role = modifiers.include?(:abstract) ? "member_abstract" : "member"
|
|
1854
|
+
scene_text(content, left + inset, vertical, role, "start")
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
# A separator is a rule across the box, broken around its title.
|
|
1858
|
+
def rules(item, horizontal, width, baselines)
|
|
1859
|
+
item[:member_rows].each_with_index.flat_map do |content, index|
|
|
1860
|
+
rule, character = item[:member_modifiers][index]
|
|
1861
|
+
next [] unless rule == :rule
|
|
1862
|
+
|
|
1863
|
+
MemberRule.style(character,
|
|
1864
|
+
rule_segments(content, horizontal,
|
|
1865
|
+
baselines.fetch(index) - 4.0, width))
|
|
1866
|
+
end
|
|
1867
|
+
end
|
|
1868
|
+
|
|
1869
|
+
def rule_segments(content, horizontal, vertical, width)
|
|
1870
|
+
right = horizontal + width
|
|
1871
|
+
if content.empty?
|
|
1872
|
+
return [segment(horizontal, vertical, right,
|
|
1873
|
+
vertical)]
|
|
1874
|
+
end
|
|
1875
|
+
|
|
1876
|
+
gap = standard_text_width(content, font_size) / 2.0
|
|
1877
|
+
middle = horizontal + (width / 2.0)
|
|
1878
|
+
[segment(horizontal, vertical, middle - gap, vertical),
|
|
1879
|
+
segment(middle + gap, vertical, right, vertical)]
|
|
1880
|
+
end
|
|
1881
|
+
|
|
1882
|
+
# A static member is underlined, as PlantUML draws it.
|
|
1883
|
+
def underlines(item, horizontal, baselines)
|
|
1884
|
+
item[:member_rows].each_with_index.filter_map do |content, index|
|
|
1885
|
+
next unless item[:member_modifiers][index].include?(:static)
|
|
1886
|
+
|
|
1887
|
+
underline(item, horizontal, content, [index, baselines])
|
|
1888
|
+
end
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
def underline(item, horizontal, content, (index, baselines))
|
|
1892
|
+
vertical = baselines.fetch(index) + 2.0
|
|
1893
|
+
left = horizontal + member_insets(item).fetch(index)
|
|
1894
|
+
width = standard_text_width(visible_member(item, content, index),
|
|
1895
|
+
font_size)
|
|
1896
|
+
segment(left, vertical, left + width, vertical)
|
|
1897
|
+
end
|
|
1898
|
+
|
|
1899
|
+
def build_relations(diagram, boxes, nodes)
|
|
1900
|
+
by_name = boxes.to_h { |box| [box.id.delete_prefix("package-"), box] }
|
|
1901
|
+
owners = junction_owners(diagram)
|
|
1902
|
+
engine = RankChain.engine(diagram)
|
|
1903
|
+
context = { by_name: by_name, owners: owners, nodes: nodes,
|
|
1904
|
+
engine: engine }
|
|
1905
|
+
scenes = diagram.relations.each_with_index.flat_map do |rel, index|
|
|
1906
|
+
next [] if rel.layout_only?
|
|
1907
|
+
|
|
1908
|
+
relation_scenes(rel, index, context)
|
|
1909
|
+
end
|
|
1910
|
+
separate_relation_labels(scenes, boxes)
|
|
1911
|
+
end
|
|
1912
|
+
|
|
1913
|
+
def separate_relation_labels(scenes, boxes)
|
|
1914
|
+
RelationLabelSeparator.new(
|
|
1915
|
+
boxes: boxes,
|
|
1916
|
+
size: font_size * SMALL_TEXT_SCALE,
|
|
1917
|
+
width_of: method(:end_text_width),
|
|
1918
|
+
drawn_width_of: lambda { |content|
|
|
1919
|
+
Drawing::Face.width(content, size: font_size * SMALL_TEXT_SCALE)
|
|
1920
|
+
},
|
|
1921
|
+
).call(scenes)
|
|
1922
|
+
end
|
|
1923
|
+
|
|
1924
|
+
def relation_scenes(relation, index, context)
|
|
1925
|
+
by_name = context.fetch(:by_name)
|
|
1926
|
+
ends = relation_ends(relation, by_name)
|
|
1927
|
+
end_texts = EndTexts.new(relation, context.fetch(:engine))
|
|
1928
|
+
junctions = context.fetch(:owners)[index]
|
|
1929
|
+
if junctions.empty?
|
|
1930
|
+
return plain_relation_scene(relation, index, ends, end_texts)
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1933
|
+
junction_relation_scenes(relation, index,
|
|
1934
|
+
[ends, end_texts, junctions], context)
|
|
1935
|
+
end
|
|
1936
|
+
|
|
1937
|
+
def relation_ends(relation, by_name)
|
|
1938
|
+
[relation.left, relation.right].map { |name| by_name.fetch(name) }
|
|
1939
|
+
end
|
|
1940
|
+
|
|
1941
|
+
def plain_relation_scene(relation, index, ends, end_texts)
|
|
1942
|
+
[relation_scene(relation, "relation-#{index}",
|
|
1943
|
+
relation_endpoints(*ends), end_texts)]
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
def junction_relation_scenes(relation, index, details, context)
|
|
1947
|
+
ends, end_texts, junctions = details
|
|
1948
|
+
by_name = context.fetch(:by_name)
|
|
1949
|
+
junction = junction_box(context.fetch(:nodes)[index], ends,
|
|
1950
|
+
context.fetch(:engine))
|
|
1951
|
+
entries = junctions.map { |entry| [*entry, by_name] }
|
|
1952
|
+
junction_scenes(relation, index, [ends, junction, end_texts], entries)
|
|
1953
|
+
end
|
|
1954
|
+
|
|
1955
|
+
# The association classes, with their numbers, grouped by the index
|
|
1956
|
+
# of the first relation joining their pair, written either way round.
|
|
1957
|
+
def junction_owners(diagram)
|
|
1958
|
+
owners = Hash.new { |hash, index| hash[index] = [] }
|
|
1959
|
+
diagram.junctions.each_with_index do |junction, number|
|
|
1960
|
+
index = junction_relation_index(diagram.relations, junction)
|
|
1961
|
+
owners[index] << [junction, number]
|
|
1962
|
+
end
|
|
1963
|
+
owners
|
|
1964
|
+
end
|
|
1965
|
+
|
|
1966
|
+
def junction_relation_index(relations, junction)
|
|
1967
|
+
pair = [junction.from, junction.to].sort
|
|
1968
|
+
relations.index do |relation|
|
|
1969
|
+
[relation.left, relation.right].sort == pair
|
|
1970
|
+
end
|
|
1971
|
+
end
|
|
1972
|
+
|
|
1973
|
+
# The junction node, or a zero-size one at the middle of the
|
|
1974
|
+
# relation when the grid placed the classes.
|
|
1975
|
+
def junction_box(node, ends, engine)
|
|
1976
|
+
node ||= [*midpoint(*relation_endpoints(*ends).values_at(:left,
|
|
1977
|
+
:right)),
|
|
1978
|
+
0.0, 0.0]
|
|
1979
|
+
x, y, width, height = node
|
|
1980
|
+
# Smetana's legs stop at the junction node's edge; the default
|
|
1981
|
+
# engine runs one line through its middle.
|
|
1982
|
+
return junction_node(x, y, width, height) unless engine == :dot
|
|
1983
|
+
|
|
1984
|
+
Scene::Box.new(id: "junction", x: x + (width / 2),
|
|
1985
|
+
y: y + (height / 2), width: 0.0, height: 0.0,
|
|
1986
|
+
texts: [], separators: [], kind: "junction")
|
|
1987
|
+
end
|
|
1988
|
+
|
|
1989
|
+
def relation_scene(relation, id, endpoints, end_texts, style = {})
|
|
1990
|
+
sides = style.fetch(:sides, %i[left right])
|
|
1991
|
+
boxes, endpoints = qualifier_boxes(relation, end_texts, endpoints,
|
|
1992
|
+
sides)
|
|
1993
|
+
texts = relation_texts(relation, endpoints, end_texts, sides,
|
|
1994
|
+
boxes.size)
|
|
1995
|
+
drawn = styled_markers(relation, style, sides, endpoints)
|
|
1996
|
+
relation_record(relation, id, texts: texts, markers: drawn,
|
|
1997
|
+
qualifiers: boxes,
|
|
1998
|
+
path: relation_path_for(relation,
|
|
1999
|
+
endpoints))
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
def styled_markers(relation, style, sides, endpoints)
|
|
2003
|
+
relation_markers(relation, style.fetch(:markers, relation.markers),
|
|
2004
|
+
sides, endpoints)
|
|
2005
|
+
end
|
|
2006
|
+
|
|
2007
|
+
def relation_record(relation, id, **drawn)
|
|
2008
|
+
Scene::Relation.new(id: id, dashed: relation.dashed?,
|
|
2009
|
+
dotted: relation.dotted?,
|
|
2010
|
+
stroke: svg_colour(relation.color),
|
|
2011
|
+
stroke_width: relation.width, **drawn)
|
|
2012
|
+
end
|
|
2013
|
+
|
|
2014
|
+
# A one-line bracketed qualifier is a box on the class edge, and the
|
|
2015
|
+
# link starts at the box's far side.
|
|
2016
|
+
# @return [Array(Array<Scene::Box>, Hash)] the boxes and the
|
|
2017
|
+
# endpoints moved clear of them
|
|
2018
|
+
def qualifier_boxes(relation, end_texts, endpoints, sides)
|
|
2019
|
+
first, second = end_texts.sides
|
|
2020
|
+
direction = unit_vector(*endpoints.values_at(first, second))
|
|
2021
|
+
entries = qualifier_entries(end_texts, endpoints, sides, relation)
|
|
2022
|
+
placed = entries.to_h do |entry|
|
|
2023
|
+
[entry[:side], qualifier_placement(entry, endpoints, direction,
|
|
2024
|
+
first)]
|
|
2025
|
+
end
|
|
2026
|
+
[placed.values.map(&:box),
|
|
2027
|
+
endpoints.merge(placed) { |_, tip, found| found.clear(tip) }]
|
|
2028
|
+
end
|
|
2029
|
+
|
|
2030
|
+
# Only on a straight vertical, unlabelled link: a slanted or flat one
|
|
2031
|
+
# is drawn between estimated class positions and beside labels that
|
|
2032
|
+
# sit away from the reference's, so a box would land off it.
|
|
2033
|
+
def qualifier_entries(end_texts, endpoints, sides, relation)
|
|
2034
|
+
return [] unless boxable_link?(endpoints, relation)
|
|
2035
|
+
|
|
2036
|
+
end_texts.entries.select do |entry|
|
|
2037
|
+
entry[:kind] == :qualifier && entry[:pieces].one? &&
|
|
2038
|
+
sides.include?(entry[:side])
|
|
2039
|
+
end
|
|
2040
|
+
end
|
|
2041
|
+
|
|
2042
|
+
def boxable_link?(endpoints, relation)
|
|
2043
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2044
|
+
!endpoints[:control] && !relation.label &&
|
|
2045
|
+
(left[0] - right[0]).abs < AXIS_TOLERANCE
|
|
2046
|
+
end
|
|
2047
|
+
|
|
2048
|
+
def qualifier_placement(entry, endpoints, direction, first)
|
|
2049
|
+
content = entry[:pieces].first
|
|
2050
|
+
outward = entry[:side] == first ? direction : direction.map(&:-@)
|
|
2051
|
+
width = measure_text(content, font_size: font_size)[:width]
|
|
2052
|
+
QualifierBox.place(content, endpoints.fetch(entry[:side]), outward,
|
|
2053
|
+
width, font_size)
|
|
2054
|
+
end
|
|
2055
|
+
|
|
2056
|
+
# A bracketed colour is a CSS name (`red`) or bare hex digits.
|
|
2057
|
+
def svg_colour(color)
|
|
2058
|
+
return unless color
|
|
2059
|
+
|
|
2060
|
+
color.match?(/\A(?:\h{3}|\h{6}|\h{8})\z/) ? "##{color}" : color
|
|
2061
|
+
end
|
|
2062
|
+
|
|
2063
|
+
def relation_markers(relation, markers, sides, endpoints)
|
|
2064
|
+
half_width = qualified_marker_half_width(relation, endpoints)
|
|
2065
|
+
drawn_markers = marker_scenes(markers.slice(*sides), endpoints,
|
|
2066
|
+
half_width: half_width)
|
|
2067
|
+
drawn_markers << label_arrow_scene(relation, endpoints)
|
|
2068
|
+
scale_thick_markers(drawn_markers.compact, relation, endpoints)
|
|
2069
|
+
end
|
|
2070
|
+
|
|
2071
|
+
# PlantUML grows an arrowhead with the line it ends: a head on a
|
|
2072
|
+
# line thicker than 4 is as many times larger as 4 goes into it.
|
|
2073
|
+
def scale_thick_markers(markers, relation, endpoints)
|
|
2074
|
+
factor = relation.width.to_f / THICK_LINE
|
|
2075
|
+
return markers unless factor > 1.0
|
|
2076
|
+
|
|
2077
|
+
markers.each do |marker|
|
|
2078
|
+
next unless marker.shape == "polygon"
|
|
2079
|
+
|
|
2080
|
+
tip = marker_tip(marker, endpoints)
|
|
2081
|
+
marker.points = scaled_points(marker.points, tip, factor)
|
|
2082
|
+
end
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
def marker_tip(marker, endpoints)
|
|
2086
|
+
first = point_pairs(marker.points).first
|
|
2087
|
+
endpoints.values_at(:left, :right).compact.min_by do |tip|
|
|
2088
|
+
JsMath.hypot(*tip.zip(first).map { |at, from| at - from })
|
|
2089
|
+
end
|
|
2090
|
+
end
|
|
2091
|
+
|
|
2092
|
+
def scaled_points(points, tip, factor)
|
|
2093
|
+
formatted_points(point_pairs(points).map do |point|
|
|
2094
|
+
point.zip(tip).map { |at, from| from + ((at - from) * factor) }
|
|
2095
|
+
end)
|
|
2096
|
+
end
|
|
2097
|
+
|
|
2098
|
+
def point_pairs(points)
|
|
2099
|
+
points.split.map { |pair| pair.split(",").map(&:to_f) }
|
|
2100
|
+
end
|
|
2101
|
+
|
|
2102
|
+
def relation_path_for(relation, endpoints)
|
|
2103
|
+
ordered = if reversed_qualified?(relation)
|
|
2104
|
+
endpoints.merge(left: endpoints[:right],
|
|
2105
|
+
right: endpoints[:left])
|
|
2106
|
+
else
|
|
2107
|
+
endpoints
|
|
2108
|
+
end
|
|
2109
|
+
qualified_special_path(relation, ordered) ||
|
|
2110
|
+
controlled_relation_path(relation, ordered)
|
|
2111
|
+
end
|
|
2112
|
+
|
|
2113
|
+
def qualified_special_path(relation, ordered)
|
|
2114
|
+
if fan_qualifier_relation?(relation)
|
|
2115
|
+
fan_qualifier_path(relation, ordered)
|
|
2116
|
+
elsif long_qualified_vertical?(relation, ordered)
|
|
2117
|
+
long_qualified_vertical_path(ordered)
|
|
2118
|
+
end
|
|
2119
|
+
end
|
|
2120
|
+
|
|
2121
|
+
def controlled_relation_path(relation, ordered)
|
|
2122
|
+
control = qualified_label_control(relation, ordered) ||
|
|
2123
|
+
qualified_lane_control(relation, ordered)
|
|
2124
|
+
return styled_curve_path(relation, ordered) unless control
|
|
2125
|
+
|
|
2126
|
+
"M #{point(ordered[:left])} Q #{point(control)} " \
|
|
2127
|
+
"#{point(ordered[:right])}"
|
|
2128
|
+
end
|
|
2129
|
+
|
|
2130
|
+
def styled_curve_path(relation, ordered)
|
|
2131
|
+
first, last = ordered.values_at(:left, :right)
|
|
2132
|
+
controls = StyledRelationCurve.controls(relation, first, last)
|
|
2133
|
+
return relation_path(ordered) unless controls
|
|
2134
|
+
|
|
2135
|
+
"M #{point(first)} C #{point(controls.first)} " \
|
|
2136
|
+
"#{point(controls.last)} #{point(last)}"
|
|
2137
|
+
end
|
|
2138
|
+
|
|
2139
|
+
def fan_qualifier_relation?(relation)
|
|
2140
|
+
multiplicities = [relation.left_multiplicity,
|
|
2141
|
+
relation.right_multiplicity].compact
|
|
2142
|
+
relation.direction.nil? && relation.length == 2 &&
|
|
2143
|
+
multiplicities.one? && multiplicities.first.start_with?("[")
|
|
2144
|
+
end
|
|
2145
|
+
|
|
2146
|
+
def fan_qualifier_path(relation, endpoints)
|
|
2147
|
+
first, last = endpoints.values_at(:left, :right)
|
|
2148
|
+
width = standard_text_width(fan_qualifier_text(relation), font_size) +
|
|
2149
|
+
QUALIFIED_FAN_LANE_ALLOWANCE
|
|
2150
|
+
left, right = fan_lane_points(first, last, width)
|
|
2151
|
+
"M #{point(first)} L #{point(left)} L #{point(right)} " \
|
|
2152
|
+
"L #{point(last)}"
|
|
2153
|
+
end
|
|
2154
|
+
|
|
2155
|
+
def fan_qualifier_text(relation)
|
|
2156
|
+
[relation.left_multiplicity, relation.right_multiplicity]
|
|
2157
|
+
.compact.first.delete_prefix("[").delete_suffix("]")
|
|
2158
|
+
end
|
|
2159
|
+
|
|
2160
|
+
def fan_lane_points(first, last, width)
|
|
2161
|
+
centre = midpoint(first, last).first
|
|
2162
|
+
rise = (last[1] - first[1]) / 3
|
|
2163
|
+
[offset_point([centre, first[1]], -(width / 2), rise),
|
|
2164
|
+
offset_point([centre, last[1]], width / 2, -rise)]
|
|
2165
|
+
end
|
|
2166
|
+
|
|
2167
|
+
def long_qualified_vertical?(relation, endpoints)
|
|
2168
|
+
qualifier_relation?(relation) && relation.length > 2 &&
|
|
2169
|
+
steep?(*endpoints.values_at(:left, :right))
|
|
2170
|
+
end
|
|
2171
|
+
|
|
2172
|
+
def long_qualified_vertical_path(endpoints)
|
|
2173
|
+
first, last = endpoints.values_at(:left, :right)
|
|
2174
|
+
vertical = (last[1] - first[1]) / 3
|
|
2175
|
+
outgoing = offset_point(first, -QUALIFIED_LONG_CURVE_OFFSET, vertical)
|
|
2176
|
+
incoming = offset_point(last, QUALIFIED_LONG_CURVE_OFFSET, -vertical)
|
|
2177
|
+
"M #{point(first)} C #{point(outgoing)} #{point(incoming)} " \
|
|
2178
|
+
"#{point(last)}"
|
|
2179
|
+
end
|
|
2180
|
+
|
|
2181
|
+
def offset_point(origin, delta_x, delta_y)
|
|
2182
|
+
[origin[0] + delta_x, origin[1] + delta_y]
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
def qualified_lane_control(relation, endpoints)
|
|
2186
|
+
return unless single_qualifier_with_other_end?(relation)
|
|
2187
|
+
|
|
2188
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2189
|
+
return if steep?(left, right)
|
|
2190
|
+
|
|
2191
|
+
middle = midpoint(left, right)
|
|
2192
|
+
outward = if middle[1] < @qualified_route_middle
|
|
2193
|
+
-QUALIFIED_FLAT_CURVE_OFFSET
|
|
2194
|
+
else
|
|
2195
|
+
QUALIFIED_FLAT_CURVE_OFFSET
|
|
2196
|
+
end
|
|
2197
|
+
[middle[0], middle[1] + outward]
|
|
2198
|
+
end
|
|
2199
|
+
|
|
2200
|
+
def single_qualifier_with_other_end?(relation)
|
|
2201
|
+
multiplicities = [relation.left_multiplicity,
|
|
2202
|
+
relation.right_multiplicity].compact
|
|
2203
|
+
multiplicities.one? { |text| text.start_with?("[") } &&
|
|
2204
|
+
multiplicities.size > 1
|
|
2205
|
+
end
|
|
2206
|
+
|
|
2207
|
+
def reversed_qualified?(relation)
|
|
2208
|
+
qualifiers = [relation.left_multiplicity,
|
|
2209
|
+
relation.right_multiplicity]
|
|
2210
|
+
qualifiers.compact.any? { |text| text.start_with?("[") } &&
|
|
2211
|
+
%i[up left].include?(relation.direction)
|
|
2212
|
+
end
|
|
2213
|
+
|
|
2214
|
+
def qualified_label_control(relation, endpoints)
|
|
2215
|
+
return unless qualifier_relation?(relation) && label_arrow?(relation)
|
|
2216
|
+
|
|
2217
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2218
|
+
return if steep?(left, right)
|
|
2219
|
+
|
|
2220
|
+
middle = midpoint(left, right)
|
|
2221
|
+
outward = middle[1] < @qualified_route_middle ? -30.0 : 30.0
|
|
2222
|
+
[middle[0], middle[1] + outward]
|
|
2223
|
+
end
|
|
2224
|
+
|
|
2225
|
+
def qualifier_relation?(relation)
|
|
2226
|
+
[relation.left_multiplicity, relation.right_multiplicity]
|
|
2227
|
+
.compact.any? { |text| text.start_with?("[") }
|
|
2228
|
+
end
|
|
2229
|
+
|
|
2230
|
+
def qualified_marker_half_width(relation, endpoints)
|
|
2231
|
+
qualifiers = [relation.left_multiplicity,
|
|
2232
|
+
relation.right_multiplicity]
|
|
2233
|
+
if qualifiers.all? { |text| text&.start_with?("[") }
|
|
2234
|
+
QUALIFIED_DOUBLE_END_HALF_WIDTH
|
|
2235
|
+
else
|
|
2236
|
+
inheritance_marker_half_width(relation, endpoints, qualifiers)
|
|
2237
|
+
end
|
|
2238
|
+
end
|
|
2239
|
+
|
|
2240
|
+
def inheritance_marker_half_width(relation, endpoints, qualifiers)
|
|
2241
|
+
return unless @qualified_layout_active && qualifiers.compact.empty?
|
|
2242
|
+
return unless relation.markers.value?(:implementation)
|
|
2243
|
+
|
|
2244
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2245
|
+
QUALIFIED_INHERITANCE_HALF_WIDTH if steep?(left, right)
|
|
2246
|
+
end
|
|
2247
|
+
|
|
2248
|
+
def label_arrow?(relation)
|
|
2249
|
+
relation.label&.match?(/[ \t]+[<>]\z/)
|
|
2250
|
+
end
|
|
2251
|
+
|
|
2252
|
+
def label_arrow_scene(relation, endpoints)
|
|
2253
|
+
return unless qualifier_relation?(relation) && label_arrow?(relation)
|
|
2254
|
+
|
|
2255
|
+
centre, direction = label_arrow_geometry(relation, endpoints)
|
|
2256
|
+
tip = shift(centre, *direction, 5.0)
|
|
2257
|
+
base = shift(centre, *direction, -4.0)
|
|
2258
|
+
points = [tip, marker_side(base, direction, 3.0),
|
|
2259
|
+
marker_side(base, direction, -3.0)]
|
|
2260
|
+
Scene::Marker.new(shape: "polygon", filled: true,
|
|
2261
|
+
points: formatted_points(points))
|
|
2262
|
+
end
|
|
2263
|
+
|
|
2264
|
+
def label_arrow_geometry(relation, endpoints)
|
|
2265
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2266
|
+
direction = unit_vector(left, right)
|
|
2267
|
+
direction = direction.map(&:-@) if relation.label.end_with?("<")
|
|
2268
|
+
centre = qualified_label_control(relation, endpoints) ||
|
|
2269
|
+
midpoint(left, right)
|
|
2270
|
+
if steep?(left, right)
|
|
2271
|
+
centre = shift(centre, 1.0, 0.0,
|
|
2272
|
+
QUALIFIED_STEEP_ARROW_OFFSET)
|
|
2273
|
+
end
|
|
2274
|
+
[centre, direction]
|
|
2275
|
+
end
|
|
2276
|
+
|
|
2277
|
+
# PlantUML turns a relation carrying an association class into two
|
|
2278
|
+
# legs meeting at a junction node, each keeping its own end's text,
|
|
2279
|
+
# plus a dashed link from that node to each association class. An
|
|
2280
|
+
# `up` or `left` word moves each end's marker to the other leg, as
|
|
2281
|
+
# the pinned references draw it.
|
|
2282
|
+
def junction_scenes(relation, index, boxes, owners)
|
|
2283
|
+
(from, to), junction, end_texts = boxes
|
|
2284
|
+
legs = junction_legs(relation, index, [from, junction, to],
|
|
2285
|
+
end_texts)
|
|
2286
|
+
legs.first.markers << junction_dot(junction)
|
|
2287
|
+
[*legs, *owners.map { |entry| owner_link(junction, *entry) }]
|
|
2288
|
+
end
|
|
2289
|
+
|
|
2290
|
+
def junction_legs(relation, index, (from, junction, to), end_texts)
|
|
2291
|
+
swap = %i[up left].include?(relation.direction)
|
|
2292
|
+
[[:left, "from", from, junction],
|
|
2293
|
+
[:right, "to", junction, to]].map do |side, name, tail, head|
|
|
2294
|
+
markers = junction_markers(relation.markers, swap, side)
|
|
2295
|
+
relation_scene(relation, "relation-#{index}-#{name}",
|
|
2296
|
+
relation_endpoints(tail, head), end_texts,
|
|
2297
|
+
sides: [side], markers: markers)
|
|
2298
|
+
end
|
|
2299
|
+
end
|
|
2300
|
+
|
|
2301
|
+
def junction_node(left, top, width, height)
|
|
2302
|
+
Scene::Box.new(id: "junction", x: left, y: top, width: width,
|
|
2303
|
+
height: height, texts: [], separators: [],
|
|
2304
|
+
kind: "junction")
|
|
2305
|
+
end
|
|
2306
|
+
|
|
2307
|
+
# The filled dot where an association class meets its relation.
|
|
2308
|
+
def junction_dot(junction)
|
|
2309
|
+
Scene::Marker.new(shape: "dot", filled: true,
|
|
2310
|
+
cx: junction.x + (junction.width / 2),
|
|
2311
|
+
cy: junction.y + (junction.height / 2),
|
|
2312
|
+
r: JUNCTION_DOT_RADIUS)
|
|
2313
|
+
end
|
|
2314
|
+
|
|
2315
|
+
def junction_markers(markers, swap, side)
|
|
2316
|
+
opposite = side == :left ? :right : :left
|
|
2317
|
+
{ side => markers[swap ? opposite : side] }
|
|
2318
|
+
end
|
|
2319
|
+
|
|
2320
|
+
def owner_link(junction, owner, number, by_name)
|
|
2321
|
+
edge = relation_endpoints(junction, by_name.fetch(owner.owner))
|
|
2322
|
+
Scene::Relation.new(id: "junction-#{number}", dashed: true,
|
|
2323
|
+
path: relation_path(edge), texts: [], markers: [])
|
|
2324
|
+
end
|
|
2325
|
+
|
|
2326
|
+
def midpoint(first, second)
|
|
2327
|
+
[(first[0] + second[0]) / 2, (first[1] + second[1]) / 2]
|
|
2328
|
+
end
|
|
2329
|
+
|
|
2330
|
+
def relation_endpoints(left_box, right_box)
|
|
2331
|
+
return self_relation_endpoints(left_box) if left_box.equal?(right_box)
|
|
2332
|
+
|
|
2333
|
+
aligned = aligned_package_endpoints(left_box, right_box)
|
|
2334
|
+
return aligned if aligned
|
|
2335
|
+
|
|
2336
|
+
{
|
|
2337
|
+
left: boundary_point(left_box, right_box),
|
|
2338
|
+
right: boundary_point(right_box, left_box),
|
|
2339
|
+
}
|
|
2340
|
+
end
|
|
2341
|
+
|
|
2342
|
+
def aligned_package_endpoints(left_box, right_box)
|
|
2343
|
+
if package_frame?(left_box)
|
|
2344
|
+
vertical_package_endpoints(left_box, right_box, :left)
|
|
2345
|
+
elsif package_frame?(right_box)
|
|
2346
|
+
vertical_package_endpoints(right_box, left_box, :right)
|
|
2347
|
+
end
|
|
2348
|
+
end
|
|
2349
|
+
|
|
2350
|
+
def package_frame?(box)
|
|
2351
|
+
box.id.start_with?("package-")
|
|
2352
|
+
end
|
|
2353
|
+
|
|
2354
|
+
def vertical_package_endpoints(frame, box, frame_side)
|
|
2355
|
+
centre = box_centre(box)
|
|
2356
|
+
return unless centre.first.between?(frame.x, frame.x + frame.width)
|
|
2357
|
+
|
|
2358
|
+
points = vertical_package_points(frame, box, centre.first)
|
|
2359
|
+
return unless points
|
|
2360
|
+
|
|
2361
|
+
frame_point, box_point = points
|
|
2362
|
+
return { left: frame_point, right: box_point } if frame_side == :left
|
|
2363
|
+
|
|
2364
|
+
{ left: box_point, right: frame_point }
|
|
2365
|
+
end
|
|
2366
|
+
|
|
2367
|
+
def vertical_package_points(frame, box, horizontal)
|
|
2368
|
+
verticals = vertical_package_edges(frame, box)
|
|
2369
|
+
verticals&.map { |vertical| [horizontal, vertical] }
|
|
2370
|
+
end
|
|
2371
|
+
|
|
2372
|
+
def vertical_package_edges(frame, box)
|
|
2373
|
+
return [bottom_of(frame), box.y] if below?(box, frame)
|
|
2374
|
+
|
|
2375
|
+
[frame.y, bottom_of(box)] if below?(frame, box)
|
|
2376
|
+
end
|
|
2377
|
+
|
|
2378
|
+
def below?(box, other)
|
|
2379
|
+
box.y >= bottom_of(other)
|
|
2380
|
+
end
|
|
2381
|
+
|
|
2382
|
+
def bottom_of(box)
|
|
2383
|
+
box.y + box.height
|
|
2384
|
+
end
|
|
2385
|
+
|
|
2386
|
+
def self_relation_endpoints(box)
|
|
2387
|
+
endpoints = { left: right_edge_point(box,
|
|
2388
|
+
SELF_RELATION_START_RATIO) }
|
|
2389
|
+
endpoints[:right] = right_edge_point(box, SELF_RELATION_END_RATIO)
|
|
2390
|
+
endpoints[:control] = right_edge_point(
|
|
2391
|
+
box, 0.5, offset: SELF_RELATION_CONTROL_OFFSET
|
|
2392
|
+
)
|
|
2393
|
+
endpoints
|
|
2394
|
+
end
|
|
2395
|
+
|
|
2396
|
+
def right_edge_point(box, height_ratio, offset: 0.0)
|
|
2397
|
+
[box.x + box.width + offset, box.y + (box.height * height_ratio)]
|
|
2398
|
+
end
|
|
2399
|
+
|
|
2400
|
+
def boundary_point(box, target)
|
|
2401
|
+
boundary_toward(box, box_centre(target))
|
|
2402
|
+
end
|
|
2403
|
+
|
|
2404
|
+
def boundary_toward(box, destination)
|
|
2405
|
+
centre = box_centre(box)
|
|
2406
|
+
delta = vector_between(centre, destination)
|
|
2407
|
+
shift(centre, *delta, boundary_scale(box, delta))
|
|
2408
|
+
end
|
|
2409
|
+
|
|
2410
|
+
def vector_between(from, to)
|
|
2411
|
+
[to[0] - from[0], to[1] - from[1]]
|
|
2412
|
+
end
|
|
2413
|
+
|
|
2414
|
+
def boundary_scale(box, delta)
|
|
2415
|
+
horizontal = axis_scale(box.width, delta[0])
|
|
2416
|
+
vertical = axis_scale(box.height, delta[1])
|
|
2417
|
+
[horizontal, vertical].compact.min
|
|
2418
|
+
end
|
|
2419
|
+
|
|
2420
|
+
def axis_scale(size, delta)
|
|
2421
|
+
return if delta.zero?
|
|
2422
|
+
|
|
2423
|
+
(size / 2) / delta.abs
|
|
2424
|
+
end
|
|
2425
|
+
|
|
2426
|
+
def box_centre(box)
|
|
2427
|
+
[box.x + (box.width / 2), box.y + (box.height / 2)]
|
|
2428
|
+
end
|
|
2429
|
+
|
|
2430
|
+
def relation_path(endpoints)
|
|
2431
|
+
left = endpoints.fetch(:left)
|
|
2432
|
+
right = endpoints.fetch(:right)
|
|
2433
|
+
return "M #{point(left)} L #{point(right)}" unless endpoints[:control]
|
|
2434
|
+
|
|
2435
|
+
control = endpoints.fetch(:control)
|
|
2436
|
+
"M #{point(left)} Q #{point(control)} #{point(right)}"
|
|
2437
|
+
end
|
|
2438
|
+
|
|
2439
|
+
def marker_scenes(markers, endpoints, half_width: nil)
|
|
2440
|
+
markers.compact.map do |side, kind|
|
|
2441
|
+
tip = endpoints.fetch(side)
|
|
2442
|
+
opposite = side == :left ? :right : :left
|
|
2443
|
+
other = endpoints[:control] || endpoints.fetch(opposite)
|
|
2444
|
+
marker_scene(kind, tip, other, half_width: half_width)
|
|
2445
|
+
end
|
|
2446
|
+
end
|
|
2447
|
+
|
|
2448
|
+
def marker_scene(kind, tip, other, half_width: nil)
|
|
2449
|
+
return nesting_scene(tip, other) if kind == :nesting
|
|
2450
|
+
|
|
2451
|
+
Scene::Marker.new(
|
|
2452
|
+
shape: "polygon", filled: FILLED_MARKERS.include?(kind),
|
|
2453
|
+
points: marker_points(tip, other, marker_shape(kind),
|
|
2454
|
+
half_width: half_width)
|
|
2455
|
+
)
|
|
2456
|
+
end
|
|
2457
|
+
|
|
2458
|
+
def nesting_scene(tip, other)
|
|
2459
|
+
centre = shift(tip, *unit_vector(tip, other), NESTING_RADIUS)
|
|
2460
|
+
Scene::Marker.new(shape: "nesting", filled: false, cx: centre[0],
|
|
2461
|
+
cy: centre[1], r: NESTING_RADIUS,
|
|
2462
|
+
cross: cross_path(centre))
|
|
2463
|
+
end
|
|
2464
|
+
|
|
2465
|
+
def cross_path(centre)
|
|
2466
|
+
x, y = centre
|
|
2467
|
+
reach = NESTING_RADIUS
|
|
2468
|
+
"M #{x - reach} #{y} L #{x + reach} #{y} " \
|
|
2469
|
+
"M #{x} #{y - reach} L #{x} #{y + reach}"
|
|
2470
|
+
end
|
|
2471
|
+
|
|
2472
|
+
def marker_shape(relation_kind)
|
|
2473
|
+
return :diamond if %i[aggregation composition].include?(relation_kind)
|
|
2474
|
+
return :arrow if ARROW_MARKERS.include?(relation_kind)
|
|
2475
|
+
|
|
2476
|
+
:triangle
|
|
2477
|
+
end
|
|
2478
|
+
|
|
2479
|
+
def marker_points(tip, other, shape, half_width: nil)
|
|
2480
|
+
direction = unit_vector(other, tip)
|
|
2481
|
+
return arrow_points(tip, direction, half_width) if shape == :arrow
|
|
2482
|
+
return diamond_points(tip, direction, half_width) if shape == :diamond
|
|
2483
|
+
|
|
2484
|
+
base = shift(tip, *direction, -MARKER_LENGTH)
|
|
2485
|
+
half_width ||= MARKER_HALF_WIDTH
|
|
2486
|
+
formatted_points(
|
|
2487
|
+
[tip, marker_side(base, direction, half_width),
|
|
2488
|
+
marker_side(base, direction, -half_width)],
|
|
2489
|
+
)
|
|
2490
|
+
end
|
|
2491
|
+
|
|
2492
|
+
# PlantUML's aggregation and composition diamond.
|
|
2493
|
+
def diamond_points(tip, direction, half_width = nil)
|
|
2494
|
+
middle = shift(tip, *direction, -(DIAMOND_LENGTH / 2))
|
|
2495
|
+
half_width ||= DIAMOND_HALF_WIDTH
|
|
2496
|
+
formatted_points(
|
|
2497
|
+
[tip, marker_side(middle, direction, half_width),
|
|
2498
|
+
shift(tip, *direction, -DIAMOND_LENGTH),
|
|
2499
|
+
marker_side(middle, direction, -half_width)],
|
|
2500
|
+
)
|
|
2501
|
+
end
|
|
2502
|
+
|
|
2503
|
+
# PlantUML's open arrowhead: two wings joined through a notch.
|
|
2504
|
+
def arrow_points(tip, direction, half_width = nil)
|
|
2505
|
+
base = shift(tip, *direction, -ARROW_LENGTH)
|
|
2506
|
+
half_width ||= ARROW_HALF_WIDTH
|
|
2507
|
+
formatted_points(
|
|
2508
|
+
[tip, marker_side(base, direction, half_width),
|
|
2509
|
+
shift(tip, *direction, -ARROW_NOTCH),
|
|
2510
|
+
marker_side(base, direction, -half_width)],
|
|
2511
|
+
)
|
|
2512
|
+
end
|
|
2513
|
+
|
|
2514
|
+
def marker_side(base, direction, distance)
|
|
2515
|
+
shift(base, -direction[1], direction[0], distance)
|
|
2516
|
+
end
|
|
2517
|
+
|
|
2518
|
+
def formatted_points(points)
|
|
2519
|
+
points.map do |coordinate|
|
|
2520
|
+
point(coordinate, separator: ",")
|
|
2521
|
+
end.join(" ")
|
|
2522
|
+
end
|
|
2523
|
+
|
|
2524
|
+
def unit_vector(from, to)
|
|
2525
|
+
delta_horizontal, delta_vertical = vector_between(from, to)
|
|
2526
|
+
length = JsMath.hypot(delta_horizontal, delta_vertical)
|
|
2527
|
+
return [1.0, 0.0] if length.zero?
|
|
2528
|
+
|
|
2529
|
+
[delta_horizontal / length, delta_vertical / length]
|
|
2530
|
+
end
|
|
2531
|
+
|
|
2532
|
+
def shift(origin, direction_horizontal, direction_vertical, distance)
|
|
2533
|
+
[origin[0] + (direction_horizontal * distance),
|
|
2534
|
+
origin[1] + (direction_vertical * distance)]
|
|
2535
|
+
end
|
|
2536
|
+
|
|
2537
|
+
# The label rides with the left end, so a relation split into legs
|
|
2538
|
+
# still shows it once.
|
|
2539
|
+
def relation_texts(relation, endpoints, end_texts, sides, boxed = 0)
|
|
2540
|
+
first, second = end_texts.sides
|
|
2541
|
+
direction = unit_vector(*endpoints.values_at(first, second))
|
|
2542
|
+
label = relation_text_label(relation, endpoints, sides)
|
|
2543
|
+
texts = positioned_end_texts(unboxed_entries(end_texts, boxed),
|
|
2544
|
+
endpoints, direction, first, sides)
|
|
2545
|
+
[label, *texts].compact
|
|
2546
|
+
end
|
|
2547
|
+
|
|
2548
|
+
def relation_text_label(relation, endpoints, sides)
|
|
2549
|
+
return unless sides.include?(:left)
|
|
2550
|
+
|
|
2551
|
+
content = visible_relation_label(relation.label)
|
|
2552
|
+
clearance = [relation.width.to_f - 1.0, 0.0].max / 2
|
|
2553
|
+
curved_relation_label(content, relation, endpoints, clearance) ||
|
|
2554
|
+
relation_label(content, endpoints, clearance)
|
|
2555
|
+
end
|
|
2556
|
+
|
|
2557
|
+
# A label on a sloping curve starts, or ends, on the side where the
|
|
2558
|
+
# curve runs away below it, so the curve does not cross the text.
|
|
2559
|
+
def curved_relation_label(content, relation, endpoints, clearance)
|
|
2560
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2561
|
+
point = StyledRelationCurve.label_point(relation, left, right)
|
|
2562
|
+
return unless content && point && !endpoints[:control]
|
|
2563
|
+
|
|
2564
|
+
descending = descending?(left, right)
|
|
2565
|
+
scene_text(content, point[0] + (descending ? LABEL_GAP : -LABEL_GAP),
|
|
2566
|
+
point[1] - LABEL_OFFSET - clearance, "relation_label",
|
|
2567
|
+
descending ? "start" : "end")
|
|
2568
|
+
end
|
|
2569
|
+
|
|
2570
|
+
def descending?(left, right)
|
|
2571
|
+
horizontal, vertical = vector_between(left, right)
|
|
2572
|
+
(horizontal * vertical).positive?
|
|
2573
|
+
end
|
|
2574
|
+
|
|
2575
|
+
def visible_relation_label(label)
|
|
2576
|
+
label&.sub(/[ \t]+[<>]\z/, "")
|
|
2577
|
+
end
|
|
2578
|
+
|
|
2579
|
+
def unboxed_entries(end_texts, boxed)
|
|
2580
|
+
return end_texts.entries if boxed.zero?
|
|
2581
|
+
|
|
2582
|
+
end_texts.entries.reject do |entry|
|
|
2583
|
+
entry[:kind] == :qualifier && entry[:pieces].one?
|
|
2584
|
+
end
|
|
2585
|
+
end
|
|
2586
|
+
|
|
2587
|
+
def positioned_end_texts(entries, endpoints, direction, first, sides)
|
|
2588
|
+
selected = entries.select { |entry| sides.include?(entry[:side]) }
|
|
2589
|
+
selected.flat_map do |entry|
|
|
2590
|
+
inward = entry[:side] == first ? 1 : -1
|
|
2591
|
+
endpoint = endpoints.fetch(entry[:side])
|
|
2592
|
+
end_text(entry, endpoint, direction, inward)
|
|
2593
|
+
end
|
|
2594
|
+
end
|
|
2595
|
+
|
|
2596
|
+
# Above the middle of a flat line; beside a steep one, centred on
|
|
2597
|
+
# its middle, as PlantUML draws a label in its own rank.
|
|
2598
|
+
def relation_label(content, endpoints, clearance = 0.0)
|
|
2599
|
+
return unless content
|
|
2600
|
+
|
|
2601
|
+
return self_relation_label(content, endpoints) if endpoints[:control]
|
|
2602
|
+
|
|
2603
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2604
|
+
if beside_label?(left, right)
|
|
2605
|
+
return steep_label_for(content, left, right, clearance)
|
|
2606
|
+
end
|
|
2607
|
+
|
|
2608
|
+
scene_text(content, midpoint(left, right).first,
|
|
2609
|
+
flat_label_baseline(content, left, right, clearance),
|
|
2610
|
+
"relation_label", "middle")
|
|
2611
|
+
end
|
|
2612
|
+
|
|
2613
|
+
def steep_label_for(content, left, right, clearance)
|
|
2614
|
+
horizontal, middle = midpoint(left, right)
|
|
2615
|
+
steep_relation_label(content, horizontal, middle,
|
|
2616
|
+
[left, right], clearance)
|
|
2617
|
+
end
|
|
2618
|
+
|
|
2619
|
+
# Above the line; higher where a slope climbs under the text's
|
|
2620
|
+
# ends, so the line does not run through it.
|
|
2621
|
+
def flat_label_baseline(content, left, right, clearance)
|
|
2622
|
+
centre, middle = midpoint(left, right)
|
|
2623
|
+
size = font_size * SMALL_TEXT_SCALE
|
|
2624
|
+
half = standard_text_width(content.to_s, size) / 2
|
|
2625
|
+
top = LabelClearance.top_between(left, right, centre - half,
|
|
2626
|
+
centre + half)
|
|
2627
|
+
[middle - LABEL_OFFSET, top - (size * TEXT_DESCENT)].min - clearance
|
|
2628
|
+
end
|
|
2629
|
+
|
|
2630
|
+
def self_relation_label(content, endpoints)
|
|
2631
|
+
horizontal = self_relation_label_horizontal(endpoints)
|
|
2632
|
+
baseline = self_relation_label_baseline(endpoints)
|
|
2633
|
+
scene_text(content, horizontal, baseline,
|
|
2634
|
+
"relation_label", "start")
|
|
2635
|
+
end
|
|
2636
|
+
|
|
2637
|
+
def self_relation_label_horizontal(endpoints)
|
|
2638
|
+
left, right, control = endpoints.values_at(:left, :right, :control)
|
|
2639
|
+
curve_right = (left[0] + (2 * control[0]) + right[0]) / 4
|
|
2640
|
+
curve_right + SELF_RELATION_LABEL_GAP
|
|
2641
|
+
end
|
|
2642
|
+
|
|
2643
|
+
def self_relation_label_baseline(endpoints)
|
|
2644
|
+
left, right = endpoints.values_at(:left, :right)
|
|
2645
|
+
vertical = midpoint(left, right).last
|
|
2646
|
+
size = font_size * SMALL_TEXT_SCALE
|
|
2647
|
+
vertical + (size * (1 - TEXT_DESCENT) / 2)
|
|
2648
|
+
end
|
|
2649
|
+
|
|
2650
|
+
def steep?(left, right)
|
|
2651
|
+
horizontal, vertical = vector_between(left, right)
|
|
2652
|
+
vertical.abs > horizontal.abs
|
|
2653
|
+
end
|
|
2654
|
+
|
|
2655
|
+
# A line this near the diagonal would run through a label centred
|
|
2656
|
+
# above its middle, so the label goes beside it.
|
|
2657
|
+
def beside_label?(left, right)
|
|
2658
|
+
horizontal, vertical = vector_between(left, right)
|
|
2659
|
+
vertical.abs > horizontal.abs * BESIDE_LABEL_SLOPE
|
|
2660
|
+
end
|
|
2661
|
+
|
|
2662
|
+
def steep_relation_label(content, horizontal, middle, line, clearance)
|
|
2663
|
+
size = font_size * SMALL_TEXT_SCALE
|
|
2664
|
+
baseline = middle + (size * (1 - TEXT_DESCENT) / 2)
|
|
2665
|
+
reach = LabelClearance.right_between(*line, baseline - size,
|
|
2666
|
+
baseline)
|
|
2667
|
+
scene_text(content, [horizontal, reach].max + clearance + LABEL_GAP,
|
|
2668
|
+
baseline,
|
|
2669
|
+
"relation_label", "start")
|
|
2670
|
+
end
|
|
2671
|
+
|
|
2672
|
+
# A multiplicity sits on the right of the line as it runs from the
|
|
2673
|
+
# link's first class to its second, and a role on its left. Each
|
|
2674
|
+
# text hugs its end, as PlantUML draws it: beside a steep line it
|
|
2675
|
+
# grows away from the line and into the gap along it, beside a flat
|
|
2676
|
+
# one into the gap and away from the line. `inward` is 1 at the
|
|
2677
|
+
# first end and -1 at the second.
|
|
2678
|
+
def end_text(entry, endpoint, direction, inward)
|
|
2679
|
+
point, sideways, upright = end_text_position(entry, endpoint,
|
|
2680
|
+
direction, inward)
|
|
2681
|
+
anchor = sideways[0].positive? ? "start" : "end"
|
|
2682
|
+
if entry[:multiline]
|
|
2683
|
+
return multiline_end_text_run(entry[:pieces],
|
|
2684
|
+
hug(point, upright[1]), anchor)
|
|
2685
|
+
end
|
|
2686
|
+
|
|
2687
|
+
end_text_run(entry[:pieces], hug(point, upright[1]), anchor)
|
|
2688
|
+
end
|
|
2689
|
+
|
|
2690
|
+
def end_text_position(entry, endpoint, direction, inward)
|
|
2691
|
+
normal, along = end_text_vectors(entry, direction, inward)
|
|
2692
|
+
point = shift(shift(endpoint, *along, END_TEXT_ALONG),
|
|
2693
|
+
*normal, END_TEXT_ACROSS)
|
|
2694
|
+
sideways, upright = if direction[1].abs >= direction[0].abs
|
|
2695
|
+
[normal, along]
|
|
2696
|
+
else
|
|
2697
|
+
[along, normal]
|
|
2698
|
+
end
|
|
2699
|
+
[point, sideways, upright]
|
|
2700
|
+
end
|
|
2701
|
+
|
|
2702
|
+
def end_text_vectors(entry, direction, inward)
|
|
2703
|
+
side = entry[:kind] == :role ? -1 : 1
|
|
2704
|
+
normal = [-direction[1] * side, direction[0] * side]
|
|
2705
|
+
inward *= -1 if entry[:kind] == :qualifier
|
|
2706
|
+
along = direction.map { |component| component * inward }
|
|
2707
|
+
[normal, along]
|
|
2708
|
+
end
|
|
2709
|
+
|
|
2710
|
+
# The baseline that puts the text just below `point`, or just above.
|
|
2711
|
+
def hug(point, downward)
|
|
2712
|
+
size = font_size * SMALL_TEXT_SCALE
|
|
2713
|
+
drop = downward.positive? ? size : -(size * TEXT_DESCENT)
|
|
2714
|
+
[point[0], point[1] + drop]
|
|
2715
|
+
end
|
|
2716
|
+
|
|
2717
|
+
# A numbered item's `1.` and its text are drawn side by side, one
|
|
2718
|
+
# space apart, as one run.
|
|
2719
|
+
def end_text_run(pieces, point, anchor)
|
|
2720
|
+
if pieces.one?
|
|
2721
|
+
return [scene_text(pieces.first, *point, "multiplicity", anchor)]
|
|
2722
|
+
end
|
|
2723
|
+
|
|
2724
|
+
numbered_end_text_run(pieces, point, anchor)
|
|
2725
|
+
end
|
|
2726
|
+
|
|
2727
|
+
def multiline_end_text_run(lines, point, anchor)
|
|
2728
|
+
line_height = font_size * SMALL_TEXT_SCALE * 1.215
|
|
2729
|
+
top = point[1] - (line_height * (lines.size - 1))
|
|
2730
|
+
lines.each_with_index.map do |line, index|
|
|
2731
|
+
scene_text(line, point[0], top + (index * line_height),
|
|
2732
|
+
"multiplicity", anchor)
|
|
2733
|
+
end
|
|
2734
|
+
end
|
|
2735
|
+
|
|
2736
|
+
def numbered_end_text_run(pieces, point, anchor)
|
|
2737
|
+
width = end_text_width(pieces.join(" "))
|
|
2738
|
+
start = point[0] - anchor_offset(anchor, width)
|
|
2739
|
+
offset = end_text_width("#{pieces.first} ")
|
|
2740
|
+
baseline = point[1]
|
|
2741
|
+
[scene_text(pieces.first, start, baseline, "multiplicity", "start"),
|
|
2742
|
+
scene_text(pieces.last, start + offset, baseline, "multiplicity",
|
|
2743
|
+
"start")]
|
|
2744
|
+
end
|
|
2745
|
+
|
|
2746
|
+
def anchor_offset(anchor, width)
|
|
2747
|
+
{ "end" => width, "middle" => width / 2 }.fetch(anchor, 0)
|
|
2748
|
+
end
|
|
2749
|
+
|
|
2750
|
+
def end_text_width(content)
|
|
2751
|
+
measure_text(content, font_size: font_size * SMALL_TEXT_SCALE)[:width]
|
|
2752
|
+
end
|
|
2753
|
+
|
|
2754
|
+
def label_widths(diagram)
|
|
2755
|
+
diagram.relations.each_with_index.filter_map do |relation, index|
|
|
2756
|
+
label = visible_relation_label(relation.label)
|
|
2757
|
+
[index, end_text_width(label)] if label
|
|
2758
|
+
end.to_h
|
|
2759
|
+
end
|
|
2760
|
+
|
|
2761
|
+
def scene_text(content, horizontal, vertical, role, anchor)
|
|
2762
|
+
Scene::Text.new(content: content, x: horizontal, y: vertical,
|
|
2763
|
+
role: role,
|
|
2764
|
+
anchor: anchor)
|
|
2765
|
+
end
|
|
2766
|
+
|
|
2767
|
+
def segment(from_horizontal, from_vertical,
|
|
2768
|
+
to_horizontal, to_vertical)
|
|
2769
|
+
Scene::Segment.new(
|
|
2770
|
+
x1: from_horizontal, y1: from_vertical,
|
|
2771
|
+
x2: to_horizontal, y2: to_vertical
|
|
2772
|
+
)
|
|
2773
|
+
end
|
|
2774
|
+
|
|
2775
|
+
def point(coordinates, separator: " ")
|
|
2776
|
+
coordinates.map { |number| number.round(3) }.join(separator)
|
|
2777
|
+
end
|
|
2778
|
+
|
|
2779
|
+
# The jar leaves one margin before the drawing and two after it; a
|
|
2780
|
+
# diagram with package frames keeps the frames' own symmetric room.
|
|
2781
|
+
def trailing_margin(diagram)
|
|
2782
|
+
diagram.packages.empty? ? MARGIN : 0.0
|
|
2783
|
+
end
|
|
2784
|
+
|
|
2785
|
+
def canvas_width(diagram, boxes)
|
|
2786
|
+
boxes.map { |box| box.x + box.width }.max + boxes.map(&:x).min +
|
|
2787
|
+
trailing_margin(diagram)
|
|
2788
|
+
end
|
|
2789
|
+
|
|
2790
|
+
def widest_box(specifications)
|
|
2791
|
+
widths = specifications.map { |item| item[:width] }
|
|
2792
|
+
widths.max
|
|
2793
|
+
end
|
|
2794
|
+
|
|
2795
|
+
def box_rows(boxes)
|
|
2796
|
+
boxes.group_by(&:y).values
|
|
2797
|
+
end
|
|
2798
|
+
|
|
2799
|
+
def canvas_height(rows)
|
|
2800
|
+
rows.sum { |row| row.map(&:height).max } +
|
|
2801
|
+
((rows.size - 1) * ROW_GAP) + (MARGIN * 2)
|
|
2802
|
+
end
|
|
2803
|
+
end
|
|
2804
|
+
end
|
|
2805
|
+
end
|
|
2806
|
+
end
|