ea 0.1.0 → 0.1.4
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 +4 -4
- data/CLAUDE.md +125 -0
- data/Rakefile +12 -4
- data/TODO.next/00-publish-blocking-bugs.md +74 -0
- data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
- data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
- data/TODO.next/03-slim-lutaml-uml.md +79 -0
- data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
- data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
- data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
- data/TODO.next/07-transformer-registry-in-factory.md +20 -0
- data/TODO.next/08-connector-type-registry.md +27 -0
- data/TODO.next/09-element-renderer-registry.md +29 -0
- data/TODO.next/10-connector-renderer-lsp.md +18 -0
- data/TODO.next/11-consolidate-style-knowledge.md +33 -0
- data/TODO.next/12-data-driven-from-db-row.md +24 -0
- data/TODO.next/13-extract-duplicated-methods.md +17 -0
- data/TODO.next/14-remove-dead-code.md +10 -0
- data/TODO.next/15-narrow-exception-handling.md +39 -0
- data/TODO.next/16-repository-indexes.md +28 -0
- data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
- data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
- data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
- data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
- data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
- data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
- data/TODO.next/23-cleanup-idallocator.md +41 -0
- data/TODO.next/24-tighten-parity-specs.md +42 -0
- data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
- data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
- data/TODO.next/27-extract-cardinality-module.md +68 -0
- data/TODO.next/28-extract-xml-sanitizer.md +51 -0
- data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
- data/TODO.next/30-struct-return-for-association-end.md +37 -0
- data/TODO.next/31-idallocator-specs.md +27 -0
- data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
- data/TODO.next/33-normalize-lower-cleanup.md +30 -0
- data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
- data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
- data/TODO.next/36-wire-interface-realization.md +50 -0
- data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
- data/config/diagram_styles.yml +200 -0
- data/config/model_transformations.yml +266 -0
- data/config/qea_schema.yml +1024 -0
- data/docs/ea_to_uml_type_mapping.md +89 -0
- data/docs/xmi_qea_conversion_capabilities.md +99 -0
- data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
- data/examples/lur/basic.lur +0 -0
- data/examples/lur/test-output.lur +0 -0
- data/examples/lur/test.lur +0 -0
- data/examples/lur_basic_usage.rb +221 -0
- data/examples/lur_cli_workflow.rb +263 -0
- data/examples/lur_statistics.rb +326 -0
- data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
- data/examples/qea/ArcGISWorkspace_template.qea +0 -0
- data/examples/qea/README_qea_parser.adoc +230 -0
- data/examples/qea/UmlModel_template.qea +0 -0
- data/examples/qea/basic.qea +0 -0
- data/examples/qea/simple.qea +0 -0
- data/examples/qea/simple_example.qea +0 -0
- data/examples/qea/test.qea +0 -0
- data/examples/qea_standalone_query.rb +73 -0
- data/examples/qea_to_repository.rb +51 -0
- data/examples/smoke_test_real_qea.rb +81 -0
- data/exe/ea +7 -0
- data/lib/ea/cli/app.rb +72 -0
- data/lib/ea/cli/command/base.rb +80 -0
- data/lib/ea/cli/command/convert.rb +62 -0
- data/lib/ea/cli/command/diagrams.rb +81 -0
- data/lib/ea/cli/command/list.rb +61 -0
- data/lib/ea/cli/command/parse.rb +29 -0
- data/lib/ea/cli/command/stats.rb +20 -0
- data/lib/ea/cli/command/validate.rb +41 -0
- data/lib/ea/cli/command.rb +15 -0
- data/lib/ea/cli/error.rb +34 -0
- data/lib/ea/cli/output/formatter.rb +34 -0
- data/lib/ea/cli/output/json_formatter.rb +20 -0
- data/lib/ea/cli/output/table_formatter.rb +42 -0
- data/lib/ea/cli/output/yaml_formatter.rb +20 -0
- data/lib/ea/cli/output.rb +56 -0
- data/lib/ea/cli.rb +17 -0
- data/lib/ea/diagram/configuration.rb +379 -0
- data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
- data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
- data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
- data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
- data/lib/ea/diagram/element_renderers.rb +43 -0
- data/lib/ea/diagram/extractor.rb +560 -0
- data/lib/ea/diagram/layout_engine.rb +170 -0
- data/lib/ea/diagram/path_builder.rb +202 -0
- data/lib/ea/diagram/style_parser.rb +42 -0
- data/lib/ea/diagram/style_resolver.rb +276 -0
- data/lib/ea/diagram/svg_renderer.rb +274 -0
- data/lib/ea/diagram/util.rb +73 -0
- data/lib/ea/diagram.rb +47 -0
- data/lib/ea/qea/benchmark.rb +210 -0
- data/lib/ea/qea/database.rb +308 -0
- data/lib/ea/qea/factory/association_builder.rb +203 -0
- data/lib/ea/qea/factory/association_transformer.rb +91 -0
- data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
- data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
- data/lib/ea/qea/factory/base_transformer.rb +177 -0
- data/lib/ea/qea/factory/class_transformer.rb +116 -0
- data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
- data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
- data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
- data/lib/ea/qea/factory/document_builder.rb +283 -0
- data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
- data/lib/ea/qea/factory/enum_transformer.rb +74 -0
- data/lib/ea/qea/factory/generalization_builder.rb +227 -0
- data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
- data/lib/ea/qea/factory/instance_transformer.rb +68 -0
- data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
- data/lib/ea/qea/factory/operation_transformer.rb +66 -0
- data/lib/ea/qea/factory/package_transformer.rb +145 -0
- data/lib/ea/qea/factory/reference_resolver.rb +99 -0
- data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
- data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
- data/lib/ea/qea/factory/transformer_registry.rb +80 -0
- data/lib/ea/qea/factory.rb +37 -0
- data/lib/ea/qea/file_detector.rb +178 -0
- data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
- data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
- data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
- data/lib/ea/qea/infrastructure.rb +12 -0
- data/lib/ea/qea/models/base_model.rb +59 -0
- data/lib/ea/qea/models/ea_attribute.rb +109 -0
- data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
- data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
- data/lib/ea/qea/models/ea_connector.rb +160 -0
- data/lib/ea/qea/models/ea_connector_type.rb +60 -0
- data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
- data/lib/ea/qea/models/ea_datatype.rb +104 -0
- data/lib/ea/qea/models/ea_diagram.rb +115 -0
- data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
- data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
- data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
- data/lib/ea/qea/models/ea_document.rb +63 -0
- data/lib/ea/qea/models/ea_object.rb +223 -0
- data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
- data/lib/ea/qea/models/ea_object_property.rb +87 -0
- data/lib/ea/qea/models/ea_object_type.rb +73 -0
- data/lib/ea/qea/models/ea_operation.rb +127 -0
- data/lib/ea/qea/models/ea_operation_param.rb +76 -0
- data/lib/ea/qea/models/ea_package.rb +78 -0
- data/lib/ea/qea/models/ea_script.rb +62 -0
- data/lib/ea/qea/models/ea_status_type.rb +66 -0
- data/lib/ea/qea/models/ea_stereotype.rb +57 -0
- data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
- data/lib/ea/qea/models/ea_xref.rb +165 -0
- data/lib/ea/qea/models.rb +35 -0
- data/lib/ea/qea/repositories/base_repository.rb +225 -0
- data/lib/ea/qea/repositories/object_repository.rb +219 -0
- data/lib/ea/qea/repositories.rb +10 -0
- data/lib/ea/qea/services/configuration.rb +211 -0
- data/lib/ea/qea/services/database_loader.rb +191 -0
- data/lib/ea/qea/services.rb +10 -0
- data/lib/ea/qea/validation/association_validator.rb +73 -0
- data/lib/ea/qea/validation/attribute_validator.rb +91 -0
- data/lib/ea/qea/validation/base_validator.rb +331 -0
- data/lib/ea/qea/validation/class_validator.rb +121 -0
- data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
- data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
- data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
- data/lib/ea/qea/validation/database.rb +16 -0
- data/lib/ea/qea/validation/diagram_validator.rb +112 -0
- data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
- data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
- data/lib/ea/qea/validation/formatters.rb +12 -0
- data/lib/ea/qea/validation/operation_validator.rb +71 -0
- data/lib/ea/qea/validation/package_validator.rb +111 -0
- data/lib/ea/qea/validation/validation_engine.rb +387 -0
- data/lib/ea/qea/validation/validation_message.rb +144 -0
- data/lib/ea/qea/validation/validation_result.rb +210 -0
- data/lib/ea/qea/validation/validator_registry.rb +134 -0
- data/lib/ea/qea/validation.rb +28 -0
- data/lib/ea/qea/verification/comparison_result.rb +264 -0
- data/lib/ea/qea/verification/document_normalizer.rb +169 -0
- data/lib/ea/qea/verification/document_verifier.rb +322 -0
- data/lib/ea/qea/verification/element_comparator.rb +277 -0
- data/lib/ea/qea/verification/structure_matcher.rb +287 -0
- data/lib/ea/qea/verification.rb +14 -0
- data/lib/ea/qea.rb +185 -0
- data/lib/ea/transformations/configuration.rb +333 -0
- data/lib/ea/transformations/format_registry.rb +366 -0
- data/lib/ea/transformations/parsers/base_parser.rb +482 -0
- data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
- data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
- data/lib/ea/transformations/transformation_engine.rb +390 -0
- data/lib/ea/transformations.rb +85 -0
- data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
- data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
- data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
- data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
- data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
- data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
- data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
- data/lib/ea/transformers/qea_to_xmi.rb +29 -0
- data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
- data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
- data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
- data/lib/ea/transformers/uml_to_xmi.rb +16 -0
- data/lib/ea/transformers.rb +34 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
- data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
- data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
- data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
- data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
- data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
- data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
- data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
- data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
- data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
- data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
- data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
- data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
- data/lib/ea/xmi/lookup_service.rb +89 -0
- data/lib/ea/xmi/parser.rb +919 -0
- data/lib/ea/xmi.rb +35 -0
- data/lib/ea.rb +10 -1
- metadata +382 -9
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Diagram
|
|
5
|
+
# Layout engine for positioning diagram elements
|
|
6
|
+
class LayoutEngine
|
|
7
|
+
include Util
|
|
8
|
+
|
|
9
|
+
DEFAULT_SPACING = 50
|
|
10
|
+
DEFAULT_PADDING = 20
|
|
11
|
+
ELEMENT_WIDTH = 120
|
|
12
|
+
ELEMENT_HEIGHT = 80
|
|
13
|
+
|
|
14
|
+
attr_reader :spacing, :element_width, :element_height
|
|
15
|
+
|
|
16
|
+
def initialize(options = {})
|
|
17
|
+
@spacing = options[:spacing] || DEFAULT_SPACING
|
|
18
|
+
@element_width = options[:element_width] || ELEMENT_WIDTH
|
|
19
|
+
@element_height = options[:element_height] || ELEMENT_HEIGHT
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def calculate_bounds(diagram_data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
23
|
+
elements = diagram_data[:elements] || []
|
|
24
|
+
return { x: 0, y: 0, width: 400, height: 300 } if elements.empty?
|
|
25
|
+
|
|
26
|
+
min_x = elements.map { |e| e[:x] || 0 }.min
|
|
27
|
+
min_y = elements.map { |e| e[:y] || 0 }.min
|
|
28
|
+
max_x = elements.map do |e|
|
|
29
|
+
(e[:x] || 0) + element_width_for(e)
|
|
30
|
+
end.max
|
|
31
|
+
max_y = elements.map do |e|
|
|
32
|
+
(e[:y] || 0) + element_height_for(e)
|
|
33
|
+
end.max
|
|
34
|
+
|
|
35
|
+
apply_padding_to_bounds(
|
|
36
|
+
{
|
|
37
|
+
x: min_x,
|
|
38
|
+
y: min_y,
|
|
39
|
+
width: max_x - min_x,
|
|
40
|
+
height: max_y - min_y,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def apply_padding_to_bounds(bounds) # rubocop:disable Metrics/AbcSize
|
|
46
|
+
padding_x = [bounds[:width] * 0.05, DEFAULT_PADDING].max
|
|
47
|
+
padding_y = [bounds[:height] * 0.05, DEFAULT_PADDING].max
|
|
48
|
+
{
|
|
49
|
+
x: bounds[:x] - padding_x,
|
|
50
|
+
y: bounds[:y] - padding_y,
|
|
51
|
+
width: bounds[:width] + (padding_x * 2),
|
|
52
|
+
height: bounds[:height] + (padding_y * 2),
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_layout(elements, connectors = []) # rubocop:disable Metrics/MethodLength
|
|
57
|
+
positioned_elements, unpositioned_elements = elements.partition do |e|
|
|
58
|
+
e[:x] && e[:y]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if unpositioned_elements.any?
|
|
62
|
+
positioned_elements += apply_force_directed_layout(
|
|
63
|
+
unpositioned_elements,
|
|
64
|
+
connectors,
|
|
65
|
+
positioned_elements,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
positioned_elements
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def calculate_element_position(element, related_elements = []) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
73
|
+
return element if element[:x] && element[:y]
|
|
74
|
+
|
|
75
|
+
if related_elements.any?
|
|
76
|
+
max_x = related_elements.map do |e|
|
|
77
|
+
(e[:x] || 0) + element_width_for(e)
|
|
78
|
+
end.max
|
|
79
|
+
element[:x] = max_x + spacing
|
|
80
|
+
element[:y] = related_elements.first[:y] || 0
|
|
81
|
+
else
|
|
82
|
+
element[:x] = 0
|
|
83
|
+
element[:y] = 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
element
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def calculate_connector_bounds(connectors) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
90
|
+
return nil if connectors.empty?
|
|
91
|
+
|
|
92
|
+
valid = connectors.select do |c|
|
|
93
|
+
c[:source_element] && c[:target_element] && c[:geometry]
|
|
94
|
+
end
|
|
95
|
+
return nil if valid.empty?
|
|
96
|
+
|
|
97
|
+
points = valid.flat_map { |conn| connector_endpoints(conn) }
|
|
98
|
+
xs = points.map(&:first)
|
|
99
|
+
ys = points.map(&:last)
|
|
100
|
+
|
|
101
|
+
{ min_x: xs.min, max_x: xs.max, min_y: ys.min, max_y: ys.max }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def connector_endpoints(conn) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
105
|
+
src = conn[:source_element]
|
|
106
|
+
tgt = conn[:target_element]
|
|
107
|
+
sx, sy, ex, ey = parse_geometry_offsets(conn[:geometry])
|
|
108
|
+
|
|
109
|
+
src_point = [(src[:x] || 0) + (src[:width] || 120) + sx,
|
|
110
|
+
(src[:y] || 0) + ((src[:height] || 80) / 2) + sy]
|
|
111
|
+
tgt_point = [(tgt[:x] || 0) + ex,
|
|
112
|
+
(tgt[:y] || 0) + ((tgt[:height] || 80) / 2) + ey]
|
|
113
|
+
|
|
114
|
+
[src_point, tgt_point]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def element_width_for(element)
|
|
118
|
+
if element[:width]
|
|
119
|
+
return element[:width].zero? ? ELEMENT_WIDTH : element[:width]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
case element[:type]
|
|
123
|
+
when "class"
|
|
124
|
+
(element[:attributes]&.size.to_i * 10) + ELEMENT_WIDTH
|
|
125
|
+
when "package"
|
|
126
|
+
ELEMENT_WIDTH + 20
|
|
127
|
+
else
|
|
128
|
+
ELEMENT_WIDTH
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def element_height_for(element)
|
|
133
|
+
if element[:height]
|
|
134
|
+
return element[:height].zero? ? ELEMENT_HEIGHT : element[:height]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
case element[:type]
|
|
138
|
+
when "class"
|
|
139
|
+
(element[:operations]&.size.to_i * 15) + ELEMENT_HEIGHT
|
|
140
|
+
when "package"
|
|
141
|
+
ELEMENT_HEIGHT - 10
|
|
142
|
+
else
|
|
143
|
+
ELEMENT_HEIGHT
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def apply_force_directed_layout(elements, _connectors, fixed_elements) # rubocop:disable Metrics/AbcSize,Metrics:MethodLength
|
|
148
|
+
positioned = []
|
|
149
|
+
elements.each_with_index do |element, index|
|
|
150
|
+
cols = Math.sqrt(elements.size).ceil
|
|
151
|
+
row = index / cols
|
|
152
|
+
col = index % cols
|
|
153
|
+
|
|
154
|
+
x = col * (ELEMENT_WIDTH + spacing)
|
|
155
|
+
y = row * (ELEMENT_HEIGHT + spacing)
|
|
156
|
+
|
|
157
|
+
if fixed_elements.any?
|
|
158
|
+
x += fixed_elements.map do |e|
|
|
159
|
+
(e[:x] || 0) + element_width_for(e)
|
|
160
|
+
end.max + (spacing * 2)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
positioned << element.merge(x: x, y: y)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
positioned
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Diagram
|
|
5
|
+
# Path builder for connector rendering
|
|
6
|
+
#
|
|
7
|
+
# This class calculates SVG path data for connectors between
|
|
8
|
+
# diagram elements, supporting various connector types and
|
|
9
|
+
# routing algorithms.
|
|
10
|
+
class PathBuilder
|
|
11
|
+
include Util
|
|
12
|
+
|
|
13
|
+
attr_reader :connector, :source_element, :target_element
|
|
14
|
+
|
|
15
|
+
def initialize(connector, source_element = nil, target_element = nil)
|
|
16
|
+
@connector = connector
|
|
17
|
+
@source_element = source_element
|
|
18
|
+
@target_element = target_element
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Build SVG path data for the connector
|
|
22
|
+
# @return [String] SVG path data
|
|
23
|
+
def build_path
|
|
24
|
+
return straight_path if simple_connector?
|
|
25
|
+
return waypoint_path if geometry_has_waypoints?
|
|
26
|
+
|
|
27
|
+
case connector[:routing_type]
|
|
28
|
+
when "orthogonal" then orthogonal_path
|
|
29
|
+
when "bezier" then bezier_path
|
|
30
|
+
else manhattan_path
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def simple_connector?
|
|
36
|
+
# Use straight line if both elements have direct coordinates
|
|
37
|
+
connector[:source_x] && connector[:source_y] &&
|
|
38
|
+
connector[:target_x] && connector[:target_y]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def straight_path
|
|
42
|
+
x1 = connector[:source_x] || 0
|
|
43
|
+
y1 = connector[:source_y] || 0
|
|
44
|
+
x2 = connector[:target_x] || 100
|
|
45
|
+
y2 = connector[:target_y] || 100
|
|
46
|
+
|
|
47
|
+
"M #{x1},#{y1} L #{x2},#{y2}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def orthogonal_path
|
|
51
|
+
# Right-angle routing
|
|
52
|
+
points = calculate_orthogonal_points
|
|
53
|
+
path_from_points(points)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def manhattan_path # rubocop:disable Metrics/MethodLength
|
|
57
|
+
# Manhattan distance routing with one bend
|
|
58
|
+
x1, y1 = source_point
|
|
59
|
+
x2, y2 = target_point
|
|
60
|
+
|
|
61
|
+
# Calculate bend point (midpoint)
|
|
62
|
+
bend_x = (x1 + x2) / 2
|
|
63
|
+
bend_y = (y1 + y2) / 2
|
|
64
|
+
|
|
65
|
+
# Choose bend direction to avoid elements
|
|
66
|
+
if (x2 - x1).abs > (y2 - y1).abs
|
|
67
|
+
# Horizontal bend
|
|
68
|
+
"M #{x1},#{y1} L #{bend_x},#{y1} L #{bend_x},#{y2} L #{x2},#{y2}"
|
|
69
|
+
else
|
|
70
|
+
# Vertical bend
|
|
71
|
+
"M #{x1},#{y1} L #{x1},#{bend_y} L #{x2},#{bend_y} L #{x2},#{y2}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def bezier_path
|
|
76
|
+
# Smooth curved path using Bezier curves
|
|
77
|
+
x1, y1 = source_point
|
|
78
|
+
x2, y2 = target_point
|
|
79
|
+
|
|
80
|
+
# Control points for smooth curve
|
|
81
|
+
cp1x = x1 + ((x2 - x1) * 0.3)
|
|
82
|
+
cp1y = y1
|
|
83
|
+
cp2x = x2 - ((x2 - x1) * 0.3)
|
|
84
|
+
cp2y = y2
|
|
85
|
+
|
|
86
|
+
"M #{x1},#{y1} C #{cp1x},#{cp1y} #{cp2x},#{cp2y} #{x2},#{y2}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def calculate_orthogonal_points # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
90
|
+
x1, y1 = source_point
|
|
91
|
+
x2, y2 = target_point
|
|
92
|
+
|
|
93
|
+
points = [[x1, y1]]
|
|
94
|
+
|
|
95
|
+
# Determine direction based on relative positions
|
|
96
|
+
if (x2 - x1).abs > (y2 - y1).abs
|
|
97
|
+
# Horizontal first, then vertical
|
|
98
|
+
points << [x1 + ((x2 - x1) / 2), y1]
|
|
99
|
+
points << [x1 + ((x2 - x1) / 2), y2]
|
|
100
|
+
else
|
|
101
|
+
# Vertical first, then horizontal
|
|
102
|
+
points << [x1, y1 + ((y2 - y1) / 2)]
|
|
103
|
+
points << [x2, y1 + ((y2 - y1) / 2)]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
points << [x2, y2]
|
|
107
|
+
points
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def path_from_points(points)
|
|
111
|
+
return "" if points.empty?
|
|
112
|
+
|
|
113
|
+
path = "M #{points[0][0]},#{points[0][1]}"
|
|
114
|
+
points[1..].each do |point|
|
|
115
|
+
path += " L #{point[0]},#{point[1]}"
|
|
116
|
+
end
|
|
117
|
+
path
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def geometry_has_waypoints?
|
|
121
|
+
return false unless connector[:geometry]
|
|
122
|
+
|
|
123
|
+
geometry_data = parse_ea_geometry(connector[:geometry])
|
|
124
|
+
geometry_data&.dig(:waypoints)&.any?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def waypoint_path
|
|
128
|
+
geometry_data = parse_ea_geometry(connector[:geometry])
|
|
129
|
+
points = []
|
|
130
|
+
|
|
131
|
+
sp = source_point
|
|
132
|
+
points << sp if sp
|
|
133
|
+
|
|
134
|
+
geometry_data[:waypoints].each do |wp|
|
|
135
|
+
points << [wp[:x], wp[:y]]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
tp = target_point
|
|
139
|
+
points << tp if tp
|
|
140
|
+
|
|
141
|
+
path_from_points(points)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def source_point
|
|
145
|
+
if connector[:source_x] && connector[:source_y]
|
|
146
|
+
[connector[:source_x], connector[:source_y]]
|
|
147
|
+
else
|
|
148
|
+
calculate_element_connection_point(source_element, :source)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def target_point
|
|
153
|
+
if connector[:target_x] && connector[:target_y]
|
|
154
|
+
[connector[:target_x], connector[:target_y]]
|
|
155
|
+
else
|
|
156
|
+
calculate_element_connection_point(target_element, :target)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def calculate_element_connection_point(element, type) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
161
|
+
return [0, 0] unless element
|
|
162
|
+
|
|
163
|
+
# Calculate connection point based on element bounds and
|
|
164
|
+
# connector type
|
|
165
|
+
x = element[:x] || 0
|
|
166
|
+
y = element[:y] || 0
|
|
167
|
+
width = element[:width] || 120
|
|
168
|
+
height = element[:height] || 80
|
|
169
|
+
|
|
170
|
+
point = case type
|
|
171
|
+
when :source
|
|
172
|
+
# Connect from right side for outgoing connectors
|
|
173
|
+
[x + width, y + (height / 2)]
|
|
174
|
+
when :target
|
|
175
|
+
# Connect to left side for incoming connectors
|
|
176
|
+
[x, y + (height / 2)]
|
|
177
|
+
else
|
|
178
|
+
[x + (width / 2), y + (height / 2)]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return point unless connector[:geometry]
|
|
182
|
+
|
|
183
|
+
# Apply relative offsets if specified
|
|
184
|
+
offsets = parse_geometry_offsets(connector[:geometry])
|
|
185
|
+
apply_offset(point, offsets, type)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def apply_offset(point, offsets, type)
|
|
189
|
+
offset_x, offset_y = case type
|
|
190
|
+
when :source
|
|
191
|
+
[offsets[0], offsets[1]]
|
|
192
|
+
when :target
|
|
193
|
+
[offsets[2], offsets[3]]
|
|
194
|
+
else
|
|
195
|
+
[0, 0]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
[point[0] + offset_x, point[1] + offset_y]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# StyleParser is a thin color utility. Style orchestration (defaults,
|
|
4
|
+
# EA-parsed overrides, connector-type dispatch) lives in StyleResolver.
|
|
5
|
+
#
|
|
6
|
+
# Historically this class held a parallel style-resolution API
|
|
7
|
+
# (`parse_element_style`, `parse_connector_style`, `get_base_element_style`,
|
|
8
|
+
# `element_specific_style`, `parse_ea_style_string`, `stereotype_style`).
|
|
9
|
+
# That API was unused — callers went through StyleResolver's
|
|
10
|
+
# `resolve_element_style` / `resolve_connector_style` instead. The duplicate
|
|
11
|
+
# API was removed on 2026-06-27 to fix the MECE violation (two style
|
|
12
|
+
# pipelines for the same concern).
|
|
13
|
+
#
|
|
14
|
+
# What remains is `color_from_ea_color`, the BGR-integer → hex color
|
|
15
|
+
# converter that StyleResolver uses when parsing EA style strings.
|
|
16
|
+
|
|
17
|
+
module Ea
|
|
18
|
+
module Diagram
|
|
19
|
+
class StyleParser
|
|
20
|
+
# EA's default fill color (light yellow) used when an EA color integer
|
|
21
|
+
# is zero / unset.
|
|
22
|
+
DEFAULT_FILL_COLOR = "#FFFFCC"
|
|
23
|
+
|
|
24
|
+
# Convert EA color integer (BGR) to hex color string.
|
|
25
|
+
#
|
|
26
|
+
# EA stores colors as BGR integers in DiagramObject.style strings
|
|
27
|
+
# (e.g. "BCol=16764159"). A zero value means "use the EA default".
|
|
28
|
+
#
|
|
29
|
+
# @param ea_color [Integer] EA BGR color value
|
|
30
|
+
# @return [String] Hex color string (e.g. "#FFFFCC")
|
|
31
|
+
def color_from_ea_color(ea_color)
|
|
32
|
+
return DEFAULT_FILL_COLOR if ea_color.zero?
|
|
33
|
+
|
|
34
|
+
b = (ea_color & 0xFF0000) >> 16
|
|
35
|
+
g = (ea_color & 0x00FF00) >> 8
|
|
36
|
+
r = ea_color & 0x0000FF
|
|
37
|
+
|
|
38
|
+
format("#%02X%02X%02X", r, g, b)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Diagram
|
|
5
|
+
# Resolves styles for diagram elements by merging multiple sources
|
|
6
|
+
#
|
|
7
|
+
# Priority order (highest to lowest):
|
|
8
|
+
# 1. EA Data from DiagramObject.style (BCol, LCol, etc.)
|
|
9
|
+
# 2. User Configuration (YAML)
|
|
10
|
+
# 3. Built-in Defaults
|
|
11
|
+
#
|
|
12
|
+
# This ensures that:
|
|
13
|
+
# - EA's original styling is preserved when present
|
|
14
|
+
# - Users can override defaults via configuration
|
|
15
|
+
# - Sensible defaults are always available
|
|
16
|
+
class StyleResolver
|
|
17
|
+
attr_reader :configuration, :style_parser
|
|
18
|
+
|
|
19
|
+
# Initialize with configuration
|
|
20
|
+
#
|
|
21
|
+
# @param config_path [String, nil] Path to configuration file
|
|
22
|
+
def initialize(config_path = nil)
|
|
23
|
+
@configuration = Configuration.new(config_path)
|
|
24
|
+
@style_parser = StyleParser.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Resolve complete style for an element
|
|
28
|
+
#
|
|
29
|
+
# @param element [Object] UML element (Class, DataType, etc.)
|
|
30
|
+
# @param diagram_object [Lutaml::Uml::DiagramObject, nil]
|
|
31
|
+
# Diagram placement data
|
|
32
|
+
# @return [Hash] Complete resolved style
|
|
33
|
+
def resolve_element_style(element, diagram_object = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
34
|
+
style = {}
|
|
35
|
+
|
|
36
|
+
# Start with configuration defaults
|
|
37
|
+
style[:fill] = configuration.style_for(element, "colors.fill")
|
|
38
|
+
style[:stroke] = configuration.style_for(element, "colors.stroke")
|
|
39
|
+
style[:stroke_width] =
|
|
40
|
+
configuration.style_for(element, "box.stroke_width")
|
|
41
|
+
style[:stroke_linecap] =
|
|
42
|
+
configuration.style_for(element, "box.stroke_linecap")
|
|
43
|
+
style[:stroke_linejoin] =
|
|
44
|
+
configuration.style_for(element, "box.stroke_linejoin")
|
|
45
|
+
style[:corner_radius] =
|
|
46
|
+
configuration.style_for(element, "box.corner_radius")
|
|
47
|
+
style[:fill_opacity] =
|
|
48
|
+
configuration.style_for(element, "box.fill_opacity")
|
|
49
|
+
style[:stroke_opacity] =
|
|
50
|
+
configuration.style_for(element, "box.stroke_opacity")
|
|
51
|
+
|
|
52
|
+
# Font configuration
|
|
53
|
+
style[:font_family] =
|
|
54
|
+
configuration.style_for(element, "fonts.class_name.family")
|
|
55
|
+
style[:font_size] =
|
|
56
|
+
configuration.style_for(element, "fonts.class_name.size")
|
|
57
|
+
style[:font_weight] =
|
|
58
|
+
configuration.style_for(element, "fonts.class_name.weight")
|
|
59
|
+
style[:font_style] =
|
|
60
|
+
configuration.style_for(element, "fonts.class_name.style")
|
|
61
|
+
|
|
62
|
+
# Override with EA data if present (highest priority)
|
|
63
|
+
if diagram_object&.style
|
|
64
|
+
ea_style = parse_diagram_object_style(diagram_object.style)
|
|
65
|
+
style.merge!(ea_style)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
style.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Resolve complete style for a connector
|
|
72
|
+
#
|
|
73
|
+
# @param connector [Object] UML connector
|
|
74
|
+
# (Association, Generalization, etc.)
|
|
75
|
+
# @param diagram_link [Lutaml::Uml::DiagramLink, nil]
|
|
76
|
+
# Diagram routing data
|
|
77
|
+
# @return [Hash] Complete resolved style
|
|
78
|
+
def resolve_connector_style(connector, diagram_link = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
79
|
+
# Determine connector type
|
|
80
|
+
connector_type = determine_connector_type(connector)
|
|
81
|
+
|
|
82
|
+
style = {}
|
|
83
|
+
|
|
84
|
+
# Start with configuration defaults for this connector type
|
|
85
|
+
style[:arrow_type] =
|
|
86
|
+
configuration.connector_style(connector_type, "arrow.type")
|
|
87
|
+
style[:arrow_size] =
|
|
88
|
+
configuration.connector_style(connector_type, "arrow.size")
|
|
89
|
+
style[:stroke] =
|
|
90
|
+
configuration.connector_style(connector_type, "line.stroke")
|
|
91
|
+
style[:stroke_width] =
|
|
92
|
+
configuration.connector_style(connector_type, "line.stroke_width")
|
|
93
|
+
style[:stroke_dasharray] =
|
|
94
|
+
configuration.connector_style(connector_type,
|
|
95
|
+
"line.stroke_dasharray")
|
|
96
|
+
style[:fill] =
|
|
97
|
+
configuration.connector_style(connector_type, "line.fill") || "none"
|
|
98
|
+
|
|
99
|
+
# Override with EA data if present (highest priority)
|
|
100
|
+
if diagram_link&.style
|
|
101
|
+
ea_style = parse_diagram_link_style(diagram_link.style)
|
|
102
|
+
style.merge!(ea_style)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
style.compact
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Resolve fill color specifically
|
|
109
|
+
#
|
|
110
|
+
# @param element [Object] UML element
|
|
111
|
+
# @param diagram_object [Lutaml::Uml::DiagramObject, nil]
|
|
112
|
+
# Diagram placement data
|
|
113
|
+
# @return [String] Resolved fill color
|
|
114
|
+
def resolve_fill_color(element, diagram_object = nil)
|
|
115
|
+
# Priority 1: EA data from DiagramObject.style
|
|
116
|
+
if diagram_object&.style
|
|
117
|
+
ea_style = parse_diagram_object_style(diagram_object.style)
|
|
118
|
+
return ea_style[:fill] if ea_style[:fill]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Priority 2: Configuration (Class > Package > Stereotype > Defaults)
|
|
122
|
+
configuration.style_for(element, "colors.fill")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Resolve stroke color specifically
|
|
126
|
+
#
|
|
127
|
+
# @param element [Object] UML element
|
|
128
|
+
# @param diagram_object [Lutaml::Uml::DiagramObject, nil]
|
|
129
|
+
# Diagram placement data
|
|
130
|
+
# @return [String] Resolved stroke color
|
|
131
|
+
def resolve_stroke_color(element, diagram_object = nil)
|
|
132
|
+
# Priority 1: EA data from DiagramObject.style
|
|
133
|
+
if diagram_object&.style
|
|
134
|
+
ea_style = parse_diagram_object_style(diagram_object.style)
|
|
135
|
+
return ea_style[:stroke] if ea_style[:stroke]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Priority 2: Configuration
|
|
139
|
+
configuration.style_for(element, "colors.stroke")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Resolve font properties
|
|
143
|
+
#
|
|
144
|
+
# @param element [Object] UML element
|
|
145
|
+
# @param context [Symbol] Font context
|
|
146
|
+
# (:class_name, :attribute, :operation, :stereotype)
|
|
147
|
+
# @return [Hash] Font properties (family, size, weight, style)
|
|
148
|
+
def resolve_font(element, context = :class_name)
|
|
149
|
+
{
|
|
150
|
+
family: configuration.style_for(element, "fonts.#{context}.family"),
|
|
151
|
+
size: configuration.style_for(element, "fonts.#{context}.size"),
|
|
152
|
+
weight: configuration.style_for(element, "fonts.#{context}.weight"),
|
|
153
|
+
style: configuration.style_for(element, "fonts.#{context}.style"),
|
|
154
|
+
}.compact
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Parse DiagramObject.style string
|
|
159
|
+
# (EA format: "BCol=16764159;LCol=0;SOID=123")
|
|
160
|
+
#
|
|
161
|
+
# @param style_string [String] EA style string
|
|
162
|
+
# @return [Hash] Parsed style with fill and stroke colors
|
|
163
|
+
def parse_diagram_object_style(style_string) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
164
|
+
return {} unless style_string
|
|
165
|
+
|
|
166
|
+
style = {}
|
|
167
|
+
pairs = style_string.split(";")
|
|
168
|
+
|
|
169
|
+
pairs.each do |pair|
|
|
170
|
+
key, value = pair.split("=", 2)
|
|
171
|
+
next unless key && value
|
|
172
|
+
|
|
173
|
+
case key.strip
|
|
174
|
+
when "BCol"
|
|
175
|
+
# Background color (BGR integer)
|
|
176
|
+
style[:fill] =
|
|
177
|
+
style_parser.color_from_ea_color(value.to_i)
|
|
178
|
+
when "LCol"
|
|
179
|
+
# Line color (BGR integer)
|
|
180
|
+
style[:stroke] =
|
|
181
|
+
style_parser.color_from_ea_color(value.to_i)
|
|
182
|
+
when "BFol"
|
|
183
|
+
# Bold font (0 or 1)
|
|
184
|
+
style[:font_weight] = value == "1" ? 700 : 400
|
|
185
|
+
when "IFol"
|
|
186
|
+
# Italic font (0 or 1)
|
|
187
|
+
style[:font_style] = value == "1" ? "italic" : "normal"
|
|
188
|
+
when "LWth"
|
|
189
|
+
# Line width
|
|
190
|
+
style[:stroke_width] = value.to_i
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
style
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Parse DiagramLink.style string
|
|
198
|
+
#
|
|
199
|
+
# @param style_string [String] EA style string
|
|
200
|
+
# @return [Hash] Parsed style
|
|
201
|
+
def parse_diagram_link_style(style_string) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
|
|
202
|
+
return {} unless style_string
|
|
203
|
+
|
|
204
|
+
style = {}
|
|
205
|
+
pairs = style_string.split(";")
|
|
206
|
+
|
|
207
|
+
pairs.each do |pair|
|
|
208
|
+
key, value = pair.split("=", 2)
|
|
209
|
+
next unless key && value
|
|
210
|
+
|
|
211
|
+
case key.strip
|
|
212
|
+
when "LCol"
|
|
213
|
+
# Line color
|
|
214
|
+
style[:stroke] =
|
|
215
|
+
style_parser.color_from_ea_color(value.to_i)
|
|
216
|
+
when "LWth"
|
|
217
|
+
# Line width
|
|
218
|
+
style[:stroke_width] = value.to_i
|
|
219
|
+
when "LStyle"
|
|
220
|
+
# Line style (0=solid, 1=dash, 2=dot, etc.)
|
|
221
|
+
case value.to_i
|
|
222
|
+
when 1
|
|
223
|
+
style[:stroke_dasharray] = "5,5"
|
|
224
|
+
when 2
|
|
225
|
+
style[:stroke_dasharray] = "2,2"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
style
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Maps UML connector classes to their style type names.
|
|
234
|
+
# New connector types are added here — no method changes needed.
|
|
235
|
+
CONNECTOR_TYPE_MAP = {
|
|
236
|
+
Lutaml::Uml::Generalization => "generalization",
|
|
237
|
+
Lutaml::Uml::Association => "association",
|
|
238
|
+
Lutaml::Uml::Dependency => "dependency",
|
|
239
|
+
Lutaml::Uml::Realization => "realization",
|
|
240
|
+
}.freeze
|
|
241
|
+
|
|
242
|
+
# Association sub-type precedence for style resolution
|
|
243
|
+
ASSOCIATION_SUBTYPE_MAP = {
|
|
244
|
+
"aggregation" => "aggregation",
|
|
245
|
+
"composition" => "composition",
|
|
246
|
+
}.freeze
|
|
247
|
+
|
|
248
|
+
# Determine connector type from connector object
|
|
249
|
+
# @param connector [Object] UML connector
|
|
250
|
+
# @return [String] Connector type name
|
|
251
|
+
def determine_connector_type(connector)
|
|
252
|
+
return "association" unless connector
|
|
253
|
+
|
|
254
|
+
type_name = CONNECTOR_TYPE_MAP[connector.class]
|
|
255
|
+
return type_name if type_name && type_name != "association"
|
|
256
|
+
return determine_association_type(connector) if type_name == "association"
|
|
257
|
+
|
|
258
|
+
"association"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Determine specific association type
|
|
262
|
+
# @param connector [Object] Association connector
|
|
263
|
+
# @return [String] Specific association type
|
|
264
|
+
def determine_association_type(connector)
|
|
265
|
+
return "association" unless connector.is_a?(Lutaml::Uml::Association)
|
|
266
|
+
|
|
267
|
+
[connector.owner_end_type, connector.member_end_type].each do |type|
|
|
268
|
+
resolved = ASSOCIATION_SUBTYPE_MAP[type&.downcase]
|
|
269
|
+
return resolved if resolved
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
"association"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|