ea 0.2.5 → 0.3.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 +4 -4
- data/TODO.refactor/01-ea-model-domain-layer.md +64 -0
- data/TODO.refactor/02-ea-model-qea-source-adapter.md +58 -0
- data/TODO.refactor/03-ea-model-xmi-source-adapter.md +33 -0
- data/TODO.refactor/04-ea-model-annotations-tagged-values.md +34 -0
- data/TODO.refactor/05-ea-model-diagrams-coordinates.md +37 -0
- data/TODO.refactor/06-spa-projection-layer.md +45 -0
- data/TODO.refactor/07-spa-sharded-output-strategy.md +28 -0
- data/TODO.refactor/08-spa-build-time-search-index.md +24 -0
- data/TODO.refactor/09-spa-frontend-data-contract.md +25 -0
- data/TODO.refactor/10-spa-cli-command-rework.md +34 -0
- data/TODO.refactor/11-spa-perf-benchmark.md +23 -0
- data/TODO.refactor/12-lutaml-uml-static-site-deprecation.md +28 -0
- data/lib/ea/cli/app.rb +13 -7
- data/lib/ea/cli/command/spa.rb +33 -48
- data/lib/ea/cli/command/svg.rb +68 -0
- data/lib/ea/cli/command.rb +1 -0
- data/lib/ea/model/annotation.rb +29 -0
- data/lib/ea/model/association.rb +40 -0
- data/lib/ea/model/base.rb +27 -0
- data/lib/ea/model/bounds.rb +22 -0
- data/lib/ea/model/classifier.rb +41 -0
- data/lib/ea/model/data_type.rb +9 -0
- data/lib/ea/model/dependency.rb +19 -0
- data/lib/ea/model/diagram.rb +28 -0
- data/lib/ea/model/diagram_connector.rb +30 -0
- data/lib/ea/model/diagram_element.rb +23 -0
- data/lib/ea/model/document.rb +107 -0
- data/lib/ea/model/enumeration.rb +15 -0
- data/lib/ea/model/enumeration_literal.rb +16 -0
- data/lib/ea/model/generalization.rb +19 -0
- data/lib/ea/model/interface.rb +9 -0
- data/lib/ea/model/klass.rb +16 -0
- data/lib/ea/model/metadata.rb +31 -0
- data/lib/ea/model/operation.rb +33 -0
- data/lib/ea/model/package.rb +36 -0
- data/lib/ea/model/parameter.rb +24 -0
- data/lib/ea/model/point.rb +16 -0
- data/lib/ea/model/polymorphic_maps.rb +30 -0
- data/lib/ea/model/primitive_type.rb +9 -0
- data/lib/ea/model/property.rb +49 -0
- data/lib/ea/model/realization.rb +19 -0
- data/lib/ea/model/relationship.rb +21 -0
- data/lib/ea/model/stereotype.rb +24 -0
- data/lib/ea/model/tagged_value.rb +20 -0
- data/lib/ea/model/waypoint.rb +19 -0
- data/lib/ea/model.rb +48 -0
- data/lib/ea/sources/qea/adapter.rb +63 -0
- data/lib/ea/sources/qea/annotation_builder.rb +36 -0
- data/lib/ea/sources/qea/classifier_builder.rb +119 -0
- data/lib/ea/sources/qea/connector_relationship_map.rb +42 -0
- data/lib/ea/sources/qea/diagram_builder.rb +130 -0
- data/lib/ea/sources/qea/diagram_style_parser.rb +43 -0
- data/lib/ea/sources/qea/id_normalizer.rb +24 -0
- data/lib/ea/sources/qea/metadata_builder.rb +42 -0
- data/lib/ea/sources/qea/object_classifier_map.rb +36 -0
- data/lib/ea/sources/qea/operation_builder.rb +79 -0
- data/lib/ea/sources/qea/package_builder.rb +44 -0
- data/lib/ea/sources/qea/property_builder.rb +86 -0
- data/lib/ea/sources/qea/relationship_builder.rb +113 -0
- data/lib/ea/sources/qea/stereotype_builder.rb +44 -0
- data/lib/ea/sources/qea/tagged_value_builder.rb +39 -0
- data/lib/ea/sources/qea.rb +31 -0
- data/lib/ea/sources/xmi/adapter.rb +62 -0
- data/lib/ea/sources/xmi/annotation_builder.rb +49 -0
- data/lib/ea/sources/xmi/classifier_builder.rb +89 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +82 -0
- data/lib/ea/sources/xmi/id_normalizer.rb +27 -0
- data/lib/ea/sources/xmi/metadata_builder.rb +32 -0
- data/lib/ea/sources/xmi/operation_builder.rb +59 -0
- data/lib/ea/sources/xmi/package_builder.rb +73 -0
- data/lib/ea/sources/xmi/property_builder.rb +77 -0
- data/lib/ea/sources/xmi/relationship_builder.rb +142 -0
- data/lib/ea/sources/xmi/relationship_type_map.rb +34 -0
- data/lib/ea/sources/xmi/type_classifier_map.rb +33 -0
- data/lib/ea/sources/xmi.rb +29 -0
- data/lib/ea/sources.rb +15 -0
- data/lib/ea/spa/configuration.rb +96 -0
- data/lib/ea/spa/generator.rb +45 -0
- data/lib/ea/spa/lazy_ref.rb +18 -0
- data/lib/ea/spa/output/sharded_multi_file_strategy.rb +71 -0
- data/lib/ea/spa/output/single_file_strategy.rb +56 -0
- data/lib/ea/spa/output/strategy.rb +36 -0
- data/lib/ea/spa/output.rb +11 -0
- data/lib/ea/spa/package_tree.rb +15 -0
- data/lib/ea/spa/package_tree_node.rb +26 -0
- data/lib/ea/spa/projector.rb +194 -0
- data/lib/ea/spa/search_entry.rb +28 -0
- data/lib/ea/spa/search_index.rb +18 -0
- data/lib/ea/spa/shard.rb +19 -0
- data/lib/ea/spa/skeleton.rb +22 -0
- data/lib/ea/spa/skeleton_entry.rb +27 -0
- data/lib/ea/spa.rb +31 -0
- data/lib/ea/svg/bounds_calculator.rb +74 -0
- data/lib/ea/svg/connector_path.rb +46 -0
- data/lib/ea/svg/element_box.rb +71 -0
- data/lib/ea/svg/renderer.rb +70 -0
- data/lib/ea/svg/style_resolver.rb +72 -0
- data/lib/ea/svg.rb +21 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea.rb +4 -0
- metadata +97 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Spa
|
|
5
|
+
class SearchIndex < Lutaml::Model::Serializable
|
|
6
|
+
attribute :version, :string, default: -> { "1.0" }
|
|
7
|
+
attribute :fields, :string, collection: true,
|
|
8
|
+
default: -> { %w[name qualifiedName kind package content] }
|
|
9
|
+
attribute :entries, SearchEntry, collection: true, initialize_empty: true
|
|
10
|
+
|
|
11
|
+
json do
|
|
12
|
+
map "version", to: :version, render_default: true
|
|
13
|
+
map "fields", to: :fields, render_default: true
|
|
14
|
+
map "entries", to: :entries, render_empty: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/ea/spa/shard.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Spa
|
|
5
|
+
# Per-entity detail payload. Plain Ruby value object (not a
|
|
6
|
+
# lutaml-model type) because the payload is an arbitrary JSON
|
|
7
|
+
# blob whose shape depends on the model element's kind — we
|
|
8
|
+
# don't want the framework's hash-type cast getting in the way.
|
|
9
|
+
Shard = Struct.new(:id, :kind, :payload, keyword_init: true) do
|
|
10
|
+
def to_h
|
|
11
|
+
{ "id" => id, "kind" => kind, "payload" => payload }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_json(*args)
|
|
15
|
+
JSON.generate(to_h, *args)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Spa
|
|
5
|
+
# Top-level SPA skeleton. Plain Struct (not a lutaml-model
|
|
6
|
+
# type) because it's a thin transport wrapper around typed
|
|
7
|
+
# sub-objects (PackageTree, SkeletonEntry) plus a raw metadata
|
|
8
|
+
# hash extracted from the model.
|
|
9
|
+
Skeleton = Struct.new(:metadata, :package_tree, :entries, :view_extras,
|
|
10
|
+
keyword_init: true) do
|
|
11
|
+
def to_json(*args)
|
|
12
|
+
payload = {
|
|
13
|
+
"metadata" => metadata,
|
|
14
|
+
"packageTree" => package_tree,
|
|
15
|
+
"entries" => entries
|
|
16
|
+
}
|
|
17
|
+
payload["viewExtras"] = view_extras unless view_extras.nil? || view_extras.empty?
|
|
18
|
+
JSON.generate(payload, *args)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Spa
|
|
5
|
+
# One row in the SPA skeleton: the minimal info needed to
|
|
6
|
+
# render the sidebar, do name searches, and route to a shard.
|
|
7
|
+
# Everything else (properties, operations, etc.) lives in the
|
|
8
|
+
# per-entity Shard.
|
|
9
|
+
class SkeletonEntry < Lutaml::Model::Serializable
|
|
10
|
+
attribute :id, :string
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
attribute :kind, :string # class|enumeration|data_type|...
|
|
13
|
+
attribute :package_id, :string
|
|
14
|
+
attribute :qualified_name, :string
|
|
15
|
+
attribute :shard_url, :string # where to fetch the full Shard
|
|
16
|
+
|
|
17
|
+
json do
|
|
18
|
+
map "id", to: :id
|
|
19
|
+
map "name", to: :name
|
|
20
|
+
map "kind", to: :kind
|
|
21
|
+
map "packageId", to: :package_id
|
|
22
|
+
map "qualifiedName", to: :qualified_name
|
|
23
|
+
map "shardUrl", to: :shard_url
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/ea/spa.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
# Ea::Spa is the SPA consumer adapter — a one-way projection of
|
|
7
|
+
# Ea::Model into view-shaped artifacts (Skeleton, Shard,
|
|
8
|
+
# SearchIndex, PackageTree) suitable for a browser frontend.
|
|
9
|
+
#
|
|
10
|
+
# Projection vs. modeling: the SPA does not redefine what a Class
|
|
11
|
+
# is. It owns view-shaped types that refer to model elements by
|
|
12
|
+
# id and add browsing-specific metadata (shard URLs, lazy refs,
|
|
13
|
+
# search content, navigation tree position).
|
|
14
|
+
#
|
|
15
|
+
# Output strategies (single-file vs sharded) live under
|
|
16
|
+
# Ea::Spa::Output and project the same artifacts to disk.
|
|
17
|
+
module Spa
|
|
18
|
+
autoload :SkeletonEntry, "ea/spa/skeleton_entry"
|
|
19
|
+
autoload :Skeleton, "ea/spa/skeleton"
|
|
20
|
+
autoload :PackageTreeNode, "ea/spa/package_tree_node"
|
|
21
|
+
autoload :PackageTree, "ea/spa/package_tree"
|
|
22
|
+
autoload :SearchEntry, "ea/spa/search_entry"
|
|
23
|
+
autoload :SearchIndex, "ea/spa/search_index"
|
|
24
|
+
autoload :Shard, "ea/spa/shard"
|
|
25
|
+
autoload :LazyRef, "ea/spa/lazy_ref"
|
|
26
|
+
autoload :Projector, "ea/spa/projector"
|
|
27
|
+
autoload :Output, "ea/spa/output"
|
|
28
|
+
autoload :Generator, "ea/spa/generator"
|
|
29
|
+
autoload :Configuration, "ea/spa/configuration"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
# Computes the actual drawing-area bounding box of an
|
|
6
|
+
# Ea::Model::Diagram. EA stores bounds in pixel space with two
|
|
7
|
+
# quirks: element rects can be inverted (rectbottom < recttop,
|
|
8
|
+
# producing negative height), and elements/connectors can sit
|
|
9
|
+
# outside the canvas bounds. The drawing area is the union of
|
|
10
|
+
# all element rects and connector waypoint positions.
|
|
11
|
+
class BoundsCalculator
|
|
12
|
+
attr_reader :diagram
|
|
13
|
+
|
|
14
|
+
def initialize(diagram)
|
|
15
|
+
@diagram = diagram
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def compute
|
|
19
|
+
points = all_points
|
|
20
|
+
return fallback_bounds if points.empty?
|
|
21
|
+
|
|
22
|
+
xs = points.map(&:first)
|
|
23
|
+
ys = points.map(&:last)
|
|
24
|
+
Bounds.new(x: xs.min, y: ys.min, width: xs.max - xs.min,
|
|
25
|
+
height: ys.max - ys.min)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def all_points
|
|
31
|
+
points = []
|
|
32
|
+
diagram.elements.each do |elem|
|
|
33
|
+
next unless elem.bounds
|
|
34
|
+
|
|
35
|
+
b = normalize_bounds(elem.bounds)
|
|
36
|
+
points << [b.x, b.y]
|
|
37
|
+
points << [b.x + b.width, b.y + b.height]
|
|
38
|
+
end
|
|
39
|
+
diagram.connectors.each do |conn|
|
|
40
|
+
conn.waypoints.each do |wp|
|
|
41
|
+
next unless wp.position
|
|
42
|
+
|
|
43
|
+
points << [wp.position.x, wp.position.y]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
points
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# EA occasionally stores inverted rects (rectbottom < recttop).
|
|
50
|
+
# Normalize so width/height are non-negative.
|
|
51
|
+
def normalize_bounds(bounds)
|
|
52
|
+
x = bounds.x
|
|
53
|
+
y = bounds.y
|
|
54
|
+
w = bounds.width
|
|
55
|
+
h = bounds.height
|
|
56
|
+
x, w = x + w, -w if w.negative?
|
|
57
|
+
y, h = y + h, -h if h.negative?
|
|
58
|
+
Bounds.new(x: x, y: y, width: w, height: h)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fallback_bounds
|
|
62
|
+
return Bounds.new(x: 0, y: 0, width: 1, height: 1) unless diagram.bounds
|
|
63
|
+
|
|
64
|
+
Bounds.new(x: diagram.bounds.x, y: diagram.bounds.y,
|
|
65
|
+
width: diagram.bounds.width.abs,
|
|
66
|
+
height: diagram.bounds.height.abs)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Internal value type — keep private to avoid polluting the
|
|
70
|
+
# Ea::Svg namespace.
|
|
71
|
+
Bounds = Struct.new(:x, :y, :width, :height, keyword_init: true)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
# Renders one DiagramConnector as an SVG <polyline> through
|
|
6
|
+
# its waypoints. Arrowheads and per-direction routing are
|
|
7
|
+
# future work; today we emit a straight-segment polyline
|
|
8
|
+
# matching the source layout.
|
|
9
|
+
class ConnectorPath
|
|
10
|
+
attr_reader :connector
|
|
11
|
+
|
|
12
|
+
def initialize(connector)
|
|
13
|
+
@connector = connector
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render
|
|
17
|
+
points = waypoints
|
|
18
|
+
return "" if points.empty?
|
|
19
|
+
|
|
20
|
+
<<~SVG.chomp
|
|
21
|
+
<polyline points="#{points.join(' ')}"
|
|
22
|
+
fill="none" stroke="#{stroke_color}"
|
|
23
|
+
stroke-width="#{stroke_width}"/>
|
|
24
|
+
SVG
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def waypoints
|
|
30
|
+
connector.waypoints.filter_map do |wp|
|
|
31
|
+
next unless wp.position
|
|
32
|
+
|
|
33
|
+
"#{wp.position.x},#{wp.position.y}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stroke_color
|
|
38
|
+
StyleResolver.new(connector.style).stroke_color
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stroke_width
|
|
42
|
+
StyleResolver.new(connector.style).stroke_width
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Svg
|
|
7
|
+
# Renders one DiagramElement as an SVG <g> containing the rect
|
|
8
|
+
# and a name label. Class-shape rendering (with attribute /
|
|
9
|
+
# operation compartments) is future work; for now we emit a
|
|
10
|
+
# simple labeled box that mirrors EA's logical placement.
|
|
11
|
+
class ElementBox
|
|
12
|
+
attr_reader :element, :model_index
|
|
13
|
+
|
|
14
|
+
def initialize(element, model_index:)
|
|
15
|
+
@element = element
|
|
16
|
+
@model_index = model_index
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render
|
|
20
|
+
return "" unless element.bounds
|
|
21
|
+
|
|
22
|
+
b = normalize_bounds(element.bounds)
|
|
23
|
+
style = StyleResolver.new(element.style)
|
|
24
|
+
label_text = model_element_name
|
|
25
|
+
|
|
26
|
+
<<~SVG.chomp
|
|
27
|
+
<g class="element" data-element-id="#{escape(element.id)}">
|
|
28
|
+
<rect x="#{b.x}" y="#{b.y}" width="#{b.width}" height="#{b.height}"
|
|
29
|
+
fill="#{style.fill_color}" stroke="#{style.stroke_color}"
|
|
30
|
+
stroke-width="#{style.stroke_width}"/>
|
|
31
|
+
<text x="#{b.x + (b.width / 2.0)}"
|
|
32
|
+
y="#{b.y + (b.height / 2.0)}"
|
|
33
|
+
text-anchor="middle" dominant-baseline="middle"
|
|
34
|
+
fill="#{style.font_color}"
|
|
35
|
+
font-family="sans-serif" font-size="14">#{escape(label_text)}</text>
|
|
36
|
+
</g>
|
|
37
|
+
SVG
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def model_element_name
|
|
43
|
+
ref = element.model_element_ref
|
|
44
|
+
return "(unbound)" unless ref
|
|
45
|
+
|
|
46
|
+
model = model_index[ref]
|
|
47
|
+
model&.name || "(missing #{ref})"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize_bounds(bounds)
|
|
51
|
+
x = bounds.x
|
|
52
|
+
y = bounds.y
|
|
53
|
+
w = bounds.width
|
|
54
|
+
h = bounds.height
|
|
55
|
+
x, w = x + w, -w if w.negative?
|
|
56
|
+
y, h = y + h, -h if h.negative?
|
|
57
|
+
OpenStruct.new(x: x, y: y, width: w, height: h)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def escape(text)
|
|
61
|
+
return "" if text.nil?
|
|
62
|
+
|
|
63
|
+
text.to_s
|
|
64
|
+
.gsub("&", "&")
|
|
65
|
+
.gsub("<", "<")
|
|
66
|
+
.gsub(">", ">")
|
|
67
|
+
.gsub("\"", """)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Svg
|
|
7
|
+
# Renders an Ea::Model::Diagram into a standalone SVG string.
|
|
8
|
+
# Coordinates come straight from the umldi (UML Diagram
|
|
9
|
+
# Interchange) content captured by the source adapter:
|
|
10
|
+
# DiagramElement bounds and DiagramConnector waypoints.
|
|
11
|
+
#
|
|
12
|
+
# The renderer walks the diagram once, computes the union
|
|
13
|
+
# bounding box, and emits SVG in the same coordinate space
|
|
14
|
+
# (no y-flip; SVG's +y is down, same as EA's pixel space).
|
|
15
|
+
class Renderer
|
|
16
|
+
DEFAULT_PADDING = 20
|
|
17
|
+
|
|
18
|
+
attr_reader :diagram, :model_index, :options
|
|
19
|
+
|
|
20
|
+
def initialize(diagram, model_index: {}, padding: DEFAULT_PADDING)
|
|
21
|
+
@diagram = diagram
|
|
22
|
+
@model_index = model_index
|
|
23
|
+
@options = { padding: padding }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render
|
|
27
|
+
bounds = BoundsCalculator.new(diagram).compute
|
|
28
|
+
padded = pad(bounds)
|
|
29
|
+
|
|
30
|
+
<<~SVG
|
|
31
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
32
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
33
|
+
viewBox="#{padded.x} #{padded.y} #{padded.width} #{padded.height}"
|
|
34
|
+
width="#{padded.width}" height="#{padded.height}"
|
|
35
|
+
font-family="sans-serif">
|
|
36
|
+
<rect x="#{padded.x}" y="#{padded.y}"
|
|
37
|
+
width="#{padded.width}" height="#{padded.height}"
|
|
38
|
+
fill="white"/>
|
|
39
|
+
#{render_elements}
|
|
40
|
+
#{render_connectors}
|
|
41
|
+
</svg>
|
|
42
|
+
SVG
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def render_elements
|
|
48
|
+
diagram.elements.map do |elem|
|
|
49
|
+
ElementBox.new(elem, model_index: model_index).render
|
|
50
|
+
end.join("\n ")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_connectors
|
|
54
|
+
diagram.connectors.map do |conn|
|
|
55
|
+
ConnectorPath.new(conn).render
|
|
56
|
+
end.join("\n ")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def pad(bounds)
|
|
60
|
+
p = options[:padding]
|
|
61
|
+
OpenStruct.new(
|
|
62
|
+
x: bounds.x - p,
|
|
63
|
+
y: bounds.y - p,
|
|
64
|
+
width: bounds.width + (2 * p),
|
|
65
|
+
height: bounds.height + (2 * p)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
# Translates the parsed style hash (from
|
|
6
|
+
# Ea::Sources::Qea::DiagramStyleParser, or the equivalent for
|
|
7
|
+
# XMI) into SVG-friendly attributes. EA packs colors as
|
|
8
|
+
# integers (e.g. 16777215 = white); we translate to CSS hex.
|
|
9
|
+
class StyleResolver
|
|
10
|
+
DEFAULT_FILL = "#ffffff"
|
|
11
|
+
DEFAULT_STROKE = "#000000"
|
|
12
|
+
DEFAULT_FONT_COLOR = "#000000"
|
|
13
|
+
|
|
14
|
+
attr_reader :style
|
|
15
|
+
|
|
16
|
+
def initialize(style_hash)
|
|
17
|
+
@style = style_hash || {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fill_color
|
|
21
|
+
color_from_ea(style[:bcol]) || DEFAULT_FILL
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stroke_color
|
|
25
|
+
color_from_ea(style[:lcol]) || DEFAULT_STROKE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def font_color
|
|
29
|
+
color_from_ea(style[:fcol]) || DEFAULT_FONT_COLOR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def stroke_width
|
|
33
|
+
raw = style[:lwd]
|
|
34
|
+
return 1 if raw.nil? || raw.to_s.empty?
|
|
35
|
+
|
|
36
|
+
Integer(raw)
|
|
37
|
+
rescue ArgumentError
|
|
38
|
+
1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_svg_attrs
|
|
42
|
+
{
|
|
43
|
+
fill: fill_color,
|
|
44
|
+
stroke: stroke_color,
|
|
45
|
+
"stroke-width": stroke_width
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# EA stores colors as decimal integers in BGR byte order
|
|
50
|
+
# (low byte = red, high byte = blue). The high byte may hold
|
|
51
|
+
# alpha in some variants; we treat anything above 0xFFFFFF
|
|
52
|
+
# as opaque.
|
|
53
|
+
def color_from_ea(raw)
|
|
54
|
+
return nil if raw.nil? || raw.to_s.empty?
|
|
55
|
+
|
|
56
|
+
# Hex strings (e.g. from XMI): passthrough.
|
|
57
|
+
return "##{raw}" if raw.to_s.match?(/\A[0-9a-fA-F]{6}\z/)
|
|
58
|
+
|
|
59
|
+
Integer(raw.to_s)
|
|
60
|
+
rescue ArgumentError
|
|
61
|
+
nil
|
|
62
|
+
else
|
|
63
|
+
# Convert BGR integer → RGB hex.
|
|
64
|
+
value = Integer(raw)
|
|
65
|
+
b = (value >> 16) & 0xff
|
|
66
|
+
g = (value >> 8) & 0xff
|
|
67
|
+
r = value & 0xff
|
|
68
|
+
format("#%02X%02X%02X", r, g, b)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/ea/svg.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
# Ea::Svg is a consumer adapter of Ea::Model::Diagram — it
|
|
5
|
+
# projects the umldi (UML Diagram Interchange) content of a
|
|
6
|
+
# diagram into standalone SVG. Coordinates are taken straight
|
|
7
|
+
# from the source: EA's pixel space (rectleft, recttop,
|
|
8
|
+
# rectright, rectbottom, t_diagramlinks.geometry) or XMI's
|
|
9
|
+
# uml:Diagram owned_element bounds.
|
|
10
|
+
#
|
|
11
|
+
# Distinct from Ea::Diagram::SvgRenderer which operates on the
|
|
12
|
+
# legacy Lutaml::Uml::Document pipeline. This module consumes
|
|
13
|
+
# the canonical Ea::Model types only.
|
|
14
|
+
module Svg
|
|
15
|
+
autoload :BoundsCalculator, "ea/svg/bounds_calculator"
|
|
16
|
+
autoload :StyleResolver, "ea/svg/style_resolver"
|
|
17
|
+
autoload :ElementBox, "ea/svg/element_box"
|
|
18
|
+
autoload :ConnectorPath, "ea/svg/connector_path"
|
|
19
|
+
autoload :Renderer, "ea/svg/renderer"
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/ea/version.rb
CHANGED
data/lib/ea.rb
CHANGED
|
@@ -9,6 +9,10 @@ module Ea
|
|
|
9
9
|
class Error < StandardError; end
|
|
10
10
|
|
|
11
11
|
autoload :Qea, "ea/qea"
|
|
12
|
+
autoload :Model, "ea/model"
|
|
13
|
+
autoload :Sources, "ea/sources"
|
|
14
|
+
autoload :Spa, "ea/spa"
|
|
15
|
+
autoload :Svg, "ea/svg"
|
|
12
16
|
autoload :Diagram, "ea/diagram"
|
|
13
17
|
autoload :Transformations, "ea/transformations"
|
|
14
18
|
autoload :Xmi, "ea/xmi"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ea
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -234,6 +234,18 @@ files:
|
|
|
234
234
|
- TODO.next/38-xmi-parser-feature-parity.md
|
|
235
235
|
- TODO.next/39-ea-spa-cli-command.md
|
|
236
236
|
- TODO.next/40-drop-lur-only-check-in-diagrams-extract.md
|
|
237
|
+
- TODO.refactor/01-ea-model-domain-layer.md
|
|
238
|
+
- TODO.refactor/02-ea-model-qea-source-adapter.md
|
|
239
|
+
- TODO.refactor/03-ea-model-xmi-source-adapter.md
|
|
240
|
+
- TODO.refactor/04-ea-model-annotations-tagged-values.md
|
|
241
|
+
- TODO.refactor/05-ea-model-diagrams-coordinates.md
|
|
242
|
+
- TODO.refactor/06-spa-projection-layer.md
|
|
243
|
+
- TODO.refactor/07-spa-sharded-output-strategy.md
|
|
244
|
+
- TODO.refactor/08-spa-build-time-search-index.md
|
|
245
|
+
- TODO.refactor/09-spa-frontend-data-contract.md
|
|
246
|
+
- TODO.refactor/10-spa-cli-command-rework.md
|
|
247
|
+
- TODO.refactor/11-spa-perf-benchmark.md
|
|
248
|
+
- TODO.refactor/12-lutaml-uml-static-site-deprecation.md
|
|
237
249
|
- TODO.release/01-release-lutaml-uml-030.md
|
|
238
250
|
- TODO.release/02-fix-lutaml-meta-gem-requires.md
|
|
239
251
|
- TODO.release/03-release-lutaml-0110.md
|
|
@@ -281,6 +293,7 @@ files:
|
|
|
281
293
|
- lib/ea/cli/command/repository_builder.rb
|
|
282
294
|
- lib/ea/cli/command/spa.rb
|
|
283
295
|
- lib/ea/cli/command/stats.rb
|
|
296
|
+
- lib/ea/cli/command/svg.rb
|
|
284
297
|
- lib/ea/cli/command/validate.rb
|
|
285
298
|
- lib/ea/cli/error.rb
|
|
286
299
|
- lib/ea/cli/output.rb
|
|
@@ -302,6 +315,36 @@ files:
|
|
|
302
315
|
- lib/ea/diagram/style_resolver.rb
|
|
303
316
|
- lib/ea/diagram/svg_renderer.rb
|
|
304
317
|
- lib/ea/diagram/util.rb
|
|
318
|
+
- lib/ea/model.rb
|
|
319
|
+
- lib/ea/model/annotation.rb
|
|
320
|
+
- lib/ea/model/association.rb
|
|
321
|
+
- lib/ea/model/base.rb
|
|
322
|
+
- lib/ea/model/bounds.rb
|
|
323
|
+
- lib/ea/model/classifier.rb
|
|
324
|
+
- lib/ea/model/data_type.rb
|
|
325
|
+
- lib/ea/model/dependency.rb
|
|
326
|
+
- lib/ea/model/diagram.rb
|
|
327
|
+
- lib/ea/model/diagram_connector.rb
|
|
328
|
+
- lib/ea/model/diagram_element.rb
|
|
329
|
+
- lib/ea/model/document.rb
|
|
330
|
+
- lib/ea/model/enumeration.rb
|
|
331
|
+
- lib/ea/model/enumeration_literal.rb
|
|
332
|
+
- lib/ea/model/generalization.rb
|
|
333
|
+
- lib/ea/model/interface.rb
|
|
334
|
+
- lib/ea/model/klass.rb
|
|
335
|
+
- lib/ea/model/metadata.rb
|
|
336
|
+
- lib/ea/model/operation.rb
|
|
337
|
+
- lib/ea/model/package.rb
|
|
338
|
+
- lib/ea/model/parameter.rb
|
|
339
|
+
- lib/ea/model/point.rb
|
|
340
|
+
- lib/ea/model/polymorphic_maps.rb
|
|
341
|
+
- lib/ea/model/primitive_type.rb
|
|
342
|
+
- lib/ea/model/property.rb
|
|
343
|
+
- lib/ea/model/realization.rb
|
|
344
|
+
- lib/ea/model/relationship.rb
|
|
345
|
+
- lib/ea/model/stereotype.rb
|
|
346
|
+
- lib/ea/model/tagged_value.rb
|
|
347
|
+
- lib/ea/model/waypoint.rb
|
|
305
348
|
- lib/ea/qea.rb
|
|
306
349
|
- lib/ea/qea/benchmark.rb
|
|
307
350
|
- lib/ea/qea/database.rb
|
|
@@ -390,6 +433,58 @@ files:
|
|
|
390
433
|
- lib/ea/qea/verification/document_verifier.rb
|
|
391
434
|
- lib/ea/qea/verification/element_comparator.rb
|
|
392
435
|
- lib/ea/qea/verification/structure_matcher.rb
|
|
436
|
+
- lib/ea/sources.rb
|
|
437
|
+
- lib/ea/sources/qea.rb
|
|
438
|
+
- lib/ea/sources/qea/adapter.rb
|
|
439
|
+
- lib/ea/sources/qea/annotation_builder.rb
|
|
440
|
+
- lib/ea/sources/qea/classifier_builder.rb
|
|
441
|
+
- lib/ea/sources/qea/connector_relationship_map.rb
|
|
442
|
+
- lib/ea/sources/qea/diagram_builder.rb
|
|
443
|
+
- lib/ea/sources/qea/diagram_style_parser.rb
|
|
444
|
+
- lib/ea/sources/qea/id_normalizer.rb
|
|
445
|
+
- lib/ea/sources/qea/metadata_builder.rb
|
|
446
|
+
- lib/ea/sources/qea/object_classifier_map.rb
|
|
447
|
+
- lib/ea/sources/qea/operation_builder.rb
|
|
448
|
+
- lib/ea/sources/qea/package_builder.rb
|
|
449
|
+
- lib/ea/sources/qea/property_builder.rb
|
|
450
|
+
- lib/ea/sources/qea/relationship_builder.rb
|
|
451
|
+
- lib/ea/sources/qea/stereotype_builder.rb
|
|
452
|
+
- lib/ea/sources/qea/tagged_value_builder.rb
|
|
453
|
+
- lib/ea/sources/xmi.rb
|
|
454
|
+
- lib/ea/sources/xmi/adapter.rb
|
|
455
|
+
- lib/ea/sources/xmi/annotation_builder.rb
|
|
456
|
+
- lib/ea/sources/xmi/classifier_builder.rb
|
|
457
|
+
- lib/ea/sources/xmi/diagram_builder.rb
|
|
458
|
+
- lib/ea/sources/xmi/id_normalizer.rb
|
|
459
|
+
- lib/ea/sources/xmi/metadata_builder.rb
|
|
460
|
+
- lib/ea/sources/xmi/operation_builder.rb
|
|
461
|
+
- lib/ea/sources/xmi/package_builder.rb
|
|
462
|
+
- lib/ea/sources/xmi/property_builder.rb
|
|
463
|
+
- lib/ea/sources/xmi/relationship_builder.rb
|
|
464
|
+
- lib/ea/sources/xmi/relationship_type_map.rb
|
|
465
|
+
- lib/ea/sources/xmi/type_classifier_map.rb
|
|
466
|
+
- lib/ea/spa.rb
|
|
467
|
+
- lib/ea/spa/configuration.rb
|
|
468
|
+
- lib/ea/spa/generator.rb
|
|
469
|
+
- lib/ea/spa/lazy_ref.rb
|
|
470
|
+
- lib/ea/spa/output.rb
|
|
471
|
+
- lib/ea/spa/output/sharded_multi_file_strategy.rb
|
|
472
|
+
- lib/ea/spa/output/single_file_strategy.rb
|
|
473
|
+
- lib/ea/spa/output/strategy.rb
|
|
474
|
+
- lib/ea/spa/package_tree.rb
|
|
475
|
+
- lib/ea/spa/package_tree_node.rb
|
|
476
|
+
- lib/ea/spa/projector.rb
|
|
477
|
+
- lib/ea/spa/search_entry.rb
|
|
478
|
+
- lib/ea/spa/search_index.rb
|
|
479
|
+
- lib/ea/spa/shard.rb
|
|
480
|
+
- lib/ea/spa/skeleton.rb
|
|
481
|
+
- lib/ea/spa/skeleton_entry.rb
|
|
482
|
+
- lib/ea/svg.rb
|
|
483
|
+
- lib/ea/svg/bounds_calculator.rb
|
|
484
|
+
- lib/ea/svg/connector_path.rb
|
|
485
|
+
- lib/ea/svg/element_box.rb
|
|
486
|
+
- lib/ea/svg/renderer.rb
|
|
487
|
+
- lib/ea/svg/style_resolver.rb
|
|
393
488
|
- lib/ea/transformations.rb
|
|
394
489
|
- lib/ea/transformations/configuration.rb
|
|
395
490
|
- lib/ea/transformations/format_registry.rb
|