ea 0.6.3 → 0.6.5
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/CHANGELOG.md +72 -0
- data/config/themes/119.yml +4 -4
- data/frontend/dist/app.iife.js +8 -8
- data/frontend/src/App.vue +6 -2
- data/frontend/src/components/AppHeader.vue +35 -8
- data/frontend/src/components/AppSidebar.vue +2 -2
- data/frontend/src/components/ClassDetails.vue +103 -120
- data/frontend/src/components/DiagramView.vue +19 -6
- data/frontend/src/components/PackageDetails.vue +67 -26
- data/frontend/src/components/PackageTreeNode.vue +25 -20
- data/frontend/src/components/SearchResults.vue +21 -8
- data/frontend/src/components/WelcomeView.vue +2 -2
- data/frontend/src/stores/dataStore.ts +142 -48
- data/frontend/src/stores/uiStore.ts +3 -4
- data/frontend/src/types.ts +160 -163
- data/lib/ea/model/diagram_connector.rb +9 -0
- data/lib/ea/model/property.rb +6 -1
- data/lib/ea/sources/qea/classifier_builder.rb +3 -1
- data/lib/ea/sources/qea/diagram_builder.rb +91 -9
- data/lib/ea/sources/qea/property_builder.rb +12 -2
- data/lib/ea/spa/output/single_file_strategy.rb +1 -1
- data/lib/ea/spa/projector.rb +108 -6
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +9 -25
- data/lib/ea/svg/ea_emitter/compartment/header.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +5 -0
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +51 -11
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +12 -10
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +6 -15
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +25 -8
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +16 -4
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +29 -4
- data/lib/ea/svg/ea_emitter/elements.rb +70 -20
- data/lib/ea/svg/ea_emitter/font_resolver.rb +5 -6
- data/lib/ea/svg/ea_emitter/label/end_label.rb +3 -1
- data/lib/ea/svg/ea_emitter/labels.rb +38 -2
- data/lib/ea/svg/ea_emitter/text_renderer.rb +97 -8
- data/lib/ea/svg/parity/checker.rb +19 -9
- data/lib/ea/svg/parity/corpus_report.rb +83 -0
- data/lib/ea/svg/parity/differ.rb +231 -0
- data/lib/ea/svg/parity.rb +2 -0
- data/lib/ea/theme/definition.rb +3 -3
- data/lib/ea/theme/loader.rb +2 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
data/frontend/src/types.ts
CHANGED
|
@@ -1,221 +1,218 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export interface SpaStatistics {
|
|
2
|
+
packages: number
|
|
3
|
+
classes: number
|
|
4
|
+
attributes: number
|
|
5
|
+
operations: number
|
|
6
|
+
associations: number
|
|
7
|
+
diagrams: number
|
|
4
8
|
}
|
|
5
9
|
|
|
6
|
-
export interface
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type: string
|
|
10
|
-
visibility?: string
|
|
11
|
-
owner: string
|
|
12
|
-
ownerName: string
|
|
13
|
-
cardinality?: SpaCardinality
|
|
14
|
-
definition?: string
|
|
15
|
-
stereotypes: string[]
|
|
16
|
-
isStatic: boolean
|
|
17
|
-
isReadOnly: boolean
|
|
18
|
-
defaultValue?: string
|
|
10
|
+
export interface SpaLogoVariant {
|
|
11
|
+
path?: string
|
|
12
|
+
url?: string
|
|
19
13
|
}
|
|
20
14
|
|
|
21
|
-
export interface
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
role?: string
|
|
25
|
-
cardinality?: SpaCardinality
|
|
26
|
-
aggregation?: string
|
|
15
|
+
export interface SpaLogoConfig {
|
|
16
|
+
light: SpaLogoVariant
|
|
17
|
+
dark: SpaLogoVariant
|
|
27
18
|
}
|
|
28
19
|
|
|
29
|
-
export interface
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
name: string
|
|
33
|
-
type: string
|
|
34
|
-
definition?: string
|
|
35
|
-
source?: SpaAssociationEnd
|
|
36
|
-
target?: SpaAssociationEnd
|
|
20
|
+
export interface SpaLogos {
|
|
21
|
+
square?: SpaLogoConfig
|
|
22
|
+
long?: SpaLogoConfig
|
|
37
23
|
}
|
|
38
24
|
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
name: string
|
|
42
|
-
visibility?: string
|
|
43
|
-
returnType?: string
|
|
44
|
-
owner: string
|
|
45
|
-
ownerName: string
|
|
46
|
-
parameters: SpaParameter[]
|
|
47
|
-
isStatic: boolean
|
|
48
|
-
isAbstract: boolean
|
|
25
|
+
export interface SpaAppearance {
|
|
26
|
+
logos?: SpaLogos
|
|
49
27
|
}
|
|
50
28
|
|
|
51
|
-
export interface
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
export interface SpaMetadata {
|
|
30
|
+
id?: string
|
|
31
|
+
title?: string
|
|
32
|
+
description?: string
|
|
33
|
+
version?: string
|
|
34
|
+
generated?: string
|
|
35
|
+
generator?: string
|
|
36
|
+
createdDate?: string
|
|
37
|
+
modifiedDate?: string
|
|
38
|
+
sourceFormat?: string
|
|
39
|
+
sourceTool?: string
|
|
40
|
+
sourcePath?: string
|
|
41
|
+
appearance?: SpaAppearance
|
|
42
|
+
statistics?: SpaStatistics
|
|
55
43
|
}
|
|
56
44
|
|
|
57
|
-
export interface
|
|
45
|
+
export interface SpaPackageTreeNode {
|
|
46
|
+
id: string
|
|
58
47
|
name: string
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
attributeId: string
|
|
64
|
-
attribute: SpaAttribute
|
|
65
|
-
inheritedFrom: string
|
|
66
|
-
inheritedFromName: string
|
|
67
|
-
parentOrder: number
|
|
48
|
+
parentId?: string
|
|
49
|
+
childIds: string[]
|
|
50
|
+
classifierIds: string[]
|
|
51
|
+
diagramIds: string[]
|
|
68
52
|
}
|
|
69
53
|
|
|
70
|
-
export interface
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
inheritedFromName: string
|
|
74
|
-
parentOrder: number
|
|
75
|
-
localRole: string
|
|
54
|
+
export interface SpaPackageTree {
|
|
55
|
+
rootIds: string[]
|
|
56
|
+
nodes: SpaPackageTreeNode[]
|
|
76
57
|
}
|
|
77
58
|
|
|
78
|
-
export interface
|
|
59
|
+
export interface SpaSkeletonEntry {
|
|
79
60
|
id: string
|
|
80
|
-
xmiId: string
|
|
81
61
|
name: string
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
definition?: string
|
|
87
|
-
attributes: string[]
|
|
88
|
-
operations: string[]
|
|
89
|
-
associations: string[]
|
|
90
|
-
generalizations: string[]
|
|
91
|
-
specializations: string[]
|
|
92
|
-
isAbstract: boolean
|
|
93
|
-
literals: SpaLiteral[]
|
|
94
|
-
inheritedAttributes: SpaInheritedAttribute[]
|
|
95
|
-
inheritedAssociations: SpaInheritedAssociation[]
|
|
62
|
+
kind: string
|
|
63
|
+
packageId?: string
|
|
64
|
+
qualifiedName?: string
|
|
65
|
+
shardUrl: string
|
|
96
66
|
}
|
|
97
67
|
|
|
98
|
-
export interface
|
|
68
|
+
export interface SpaShard {
|
|
99
69
|
id: string
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
path: string
|
|
103
|
-
definition?: string
|
|
104
|
-
stereotypes: string[]
|
|
105
|
-
classes: string[]
|
|
106
|
-
subPackages: string[]
|
|
107
|
-
diagrams: string[]
|
|
108
|
-
parent?: string
|
|
70
|
+
kind: string
|
|
71
|
+
payload: any
|
|
109
72
|
}
|
|
110
73
|
|
|
111
|
-
export interface
|
|
74
|
+
export interface SpaSearchEntry {
|
|
112
75
|
id: string
|
|
76
|
+
kind: string
|
|
113
77
|
name: string
|
|
114
|
-
|
|
78
|
+
qualifiedName: string
|
|
79
|
+
package: string
|
|
80
|
+
content: string
|
|
81
|
+
boost?: number
|
|
115
82
|
}
|
|
116
83
|
|
|
117
|
-
export interface
|
|
84
|
+
export interface SpaSearchIndex {
|
|
85
|
+
version: string
|
|
86
|
+
fields: string[]
|
|
87
|
+
entries: SpaSearchEntry[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SpaAnnotation {
|
|
118
91
|
id: string
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
stereotypes: string[]
|
|
122
|
-
classCount: number
|
|
123
|
-
classes: SpaTreeClassRef[]
|
|
124
|
-
children: SpaPackageTreeNode[]
|
|
92
|
+
kind: string
|
|
93
|
+
body?: string
|
|
125
94
|
}
|
|
126
95
|
|
|
127
|
-
export interface
|
|
96
|
+
export interface SpaPropertyPayload {
|
|
128
97
|
id: string
|
|
129
|
-
xmiId: string
|
|
130
98
|
name: string
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
99
|
+
ownerId: string
|
|
100
|
+
typeName?: string
|
|
101
|
+
qualifiedName?: string
|
|
102
|
+
multiplicityLower?: number
|
|
103
|
+
multiplicityUpper?: number
|
|
104
|
+
isDerived: boolean
|
|
105
|
+
isReadonly: boolean
|
|
106
|
+
visibility?: string
|
|
107
|
+
stereotypeRefs: string[]
|
|
108
|
+
annotations: SpaAnnotation[]
|
|
136
109
|
}
|
|
137
110
|
|
|
138
|
-
export interface
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
111
|
+
export interface SpaParameterPayload {
|
|
112
|
+
name?: string
|
|
113
|
+
direction?: string
|
|
114
|
+
typeName?: string
|
|
115
|
+
multiplicityLower?: number
|
|
116
|
+
multiplicityUpper?: number
|
|
117
|
+
defaultValue?: string
|
|
144
118
|
}
|
|
145
119
|
|
|
146
|
-
export interface
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
export interface SpaOperationPayload {
|
|
121
|
+
id: string
|
|
122
|
+
name: string
|
|
123
|
+
ownerId: string
|
|
124
|
+
qualifiedName?: string
|
|
125
|
+
returnTypeName?: string
|
|
126
|
+
isStatic: boolean
|
|
127
|
+
isAbstract: boolean
|
|
128
|
+
visibility?: string
|
|
129
|
+
parameters: SpaParameterPayload[]
|
|
130
|
+
stereotypeRefs: string[]
|
|
131
|
+
annotations: SpaAnnotation[]
|
|
149
132
|
}
|
|
150
133
|
|
|
151
|
-
export interface
|
|
152
|
-
|
|
153
|
-
|
|
134
|
+
export interface SpaGeneralizationStub {
|
|
135
|
+
id: string
|
|
136
|
+
targetId: string
|
|
154
137
|
}
|
|
155
138
|
|
|
156
|
-
export interface
|
|
157
|
-
|
|
158
|
-
|
|
139
|
+
export interface SpaAssociationStub {
|
|
140
|
+
id: string
|
|
141
|
+
name?: string
|
|
142
|
+
sourceId: string
|
|
143
|
+
targetId: string
|
|
144
|
+
thisEndRoleName?: string
|
|
145
|
+
otherEndRoleName?: string
|
|
146
|
+
otherEndId: string
|
|
147
|
+
otherEndMultiplicity?: [number?, number?]
|
|
148
|
+
otherEndAggregation?: string
|
|
159
149
|
}
|
|
160
150
|
|
|
161
|
-
export interface
|
|
162
|
-
|
|
151
|
+
export interface SpaEnumerationLiteralPayload {
|
|
152
|
+
id: string
|
|
153
|
+
name: string
|
|
154
|
+
value?: string
|
|
155
|
+
ordinal?: number
|
|
163
156
|
}
|
|
164
157
|
|
|
165
|
-
export interface
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
export interface SpaClassifierPayload {
|
|
159
|
+
id: string
|
|
160
|
+
name: string
|
|
161
|
+
qualifiedName: string
|
|
162
|
+
packageId: string
|
|
163
|
+
packageName?: string
|
|
164
|
+
isAbstract: boolean
|
|
165
|
+
visibility?: string
|
|
166
|
+
modelKind: string
|
|
167
|
+
properties: SpaPropertyPayload[]
|
|
168
|
+
operations: SpaOperationPayload[]
|
|
169
|
+
stereotypeRefs: string[]
|
|
170
|
+
taggedValues: any[]
|
|
171
|
+
constraints: any[]
|
|
172
|
+
annotations: SpaAnnotation[]
|
|
173
|
+
literals?: SpaEnumerationLiteralPayload[]
|
|
174
|
+
generalizations?: SpaGeneralizationStub[]
|
|
175
|
+
specializations?: SpaGeneralizationStub[]
|
|
176
|
+
associations?: SpaAssociationStub[]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface SpaPackagePayload {
|
|
180
|
+
id: string
|
|
181
|
+
name: string
|
|
182
|
+
parentId?: string
|
|
183
|
+
subPackageIds: string[]
|
|
184
|
+
classifierIds: string[]
|
|
185
|
+
diagramIds: string[]
|
|
186
|
+
stereotypeRefs: string[]
|
|
187
|
+
taggedValues: any[]
|
|
188
|
+
annotations: SpaAnnotation[]
|
|
178
189
|
}
|
|
179
190
|
|
|
180
|
-
export interface
|
|
191
|
+
export interface SpaDiagramPayload {
|
|
181
192
|
id: string
|
|
182
|
-
type: string
|
|
183
|
-
entityType: string
|
|
184
|
-
entityId: string
|
|
185
193
|
name: string
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
packageId?: string
|
|
195
|
+
diagramType?: string
|
|
196
|
+
elements: any[]
|
|
197
|
+
connectors: any[]
|
|
198
|
+
annotations: SpaAnnotation[]
|
|
199
|
+
svg?: string
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
export interface
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
documentStore: SpaSearchEntry[]
|
|
197
|
-
pipeline: string[]
|
|
202
|
+
export interface SpaViewExtras {
|
|
203
|
+
ui?: Record<string, any>
|
|
204
|
+
appearance?: SpaAppearance
|
|
205
|
+
diagrams?: { enabled?: boolean }
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
export interface
|
|
208
|
+
export interface SpaSkeleton {
|
|
201
209
|
metadata: SpaMetadata
|
|
202
|
-
packageTree:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
attributes: Record<string, SpaAttribute>
|
|
206
|
-
associations: Record<string, SpaAssociation>
|
|
207
|
-
operations: Record<string, SpaOperation>
|
|
208
|
-
diagrams: Record<string, SpaDiagram>
|
|
210
|
+
packageTree: SpaPackageTree
|
|
211
|
+
entries: SpaSkeletonEntry[]
|
|
212
|
+
viewExtras?: SpaViewExtras
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
export interface SpaData {
|
|
212
|
-
metadata: SpaMetadata
|
|
213
|
-
packageTree: SpaPackageTreeNode
|
|
214
|
-
packages: Record<string, SpaPackage>
|
|
215
|
-
classes: Record<string, SpaClass>
|
|
216
|
-
attributes: Record<string, SpaAttribute>
|
|
217
|
-
associations: Record<string, SpaAssociation>
|
|
218
|
-
operations: Record<string, SpaOperation>
|
|
219
|
-
diagrams: Record<string, SpaDiagram>
|
|
215
|
+
export interface SpaData extends SpaSkeleton {
|
|
220
216
|
searchIndex: SpaSearchIndex
|
|
217
|
+
shards: SpaShard[]
|
|
221
218
|
}
|
|
@@ -33,6 +33,13 @@ module Ea
|
|
|
33
33
|
# connectors (no SX/SY) get a plain solid line.
|
|
34
34
|
attribute :has_geometry_offsets, :boolean, default: false
|
|
35
35
|
|
|
36
|
+
# Absolute connection points on the source/target element boxes,
|
|
37
|
+
# derived from the Geometry SX/SY and EX/EY edge offsets. EA
|
|
38
|
+
# docks the connector's visible endpoints at these ports; nil
|
|
39
|
+
# when the geometry string carries no offsets (auto-routed).
|
|
40
|
+
attribute :source_port, Ea::Model::Point
|
|
41
|
+
attribute :target_port, Ea::Model::Point
|
|
42
|
+
|
|
36
43
|
json do
|
|
37
44
|
map "id", to: :id
|
|
38
45
|
map "diagramId", to: :diagram_id
|
|
@@ -43,6 +50,8 @@ module Ea
|
|
|
43
50
|
map "targetDuid", to: :target_duid
|
|
44
51
|
map "sourceEdge", to: :source_edge
|
|
45
52
|
map "targetEdge", to: :target_edge
|
|
53
|
+
map "sourcePort", to: :source_port
|
|
54
|
+
map "targetPort", to: :target_port
|
|
46
55
|
map "connectorType", to: :connector_type
|
|
47
56
|
map "direction", to: :direction
|
|
48
57
|
map "waypoints", to: :waypoints, render_empty: true
|
data/lib/ea/model/property.rb
CHANGED
|
@@ -12,7 +12,11 @@ module Ea
|
|
|
12
12
|
attribute :type_name, :string # name shown when type is external/primitive
|
|
13
13
|
attribute :qualified_name, :string
|
|
14
14
|
attribute :multiplicity_lower, :integer
|
|
15
|
-
attribute :multiplicity_upper, :integer
|
|
15
|
+
attribute :multiplicity_upper, :integer
|
|
16
|
+
# Verbatim stored bound text (e.g. EA's literal "n") when the
|
|
17
|
+
# column value is non-numeric; rendered as-is by the SVG line
|
|
18
|
+
# builder while computed-unbounded uppers render as "*".
|
|
19
|
+
attribute :multiplicity_upper_raw, :string # -1 means "many"
|
|
16
20
|
attribute :default_value, :string
|
|
17
21
|
attribute :is_derived, :boolean, default: false
|
|
18
22
|
attribute :is_readonly, :boolean, default: false
|
|
@@ -33,6 +37,7 @@ module Ea
|
|
|
33
37
|
map "typeName", to: :type_name
|
|
34
38
|
map "qualifiedName", to: :qualified_name
|
|
35
39
|
map "multiplicityLower", to: :multiplicity_lower
|
|
40
|
+
map "multiplicityUpperRaw", to: :multiplicity_upper_raw
|
|
36
41
|
map "multiplicityUpper", to: :multiplicity_upper
|
|
37
42
|
map "defaultValue", to: :default_value
|
|
38
43
|
map "isDerived", to: :is_derived, render_default: true
|
|
@@ -155,8 +155,10 @@ module Ea
|
|
|
155
155
|
|
|
156
156
|
def build_assoc_property(conn, role, owner_object)
|
|
157
157
|
target = database.find_object(conn.end_object_id) if conn.end_object_id&.to_i&.positive?
|
|
158
|
+
guid = IdNormalizer.from_guid(conn.ea_guid)
|
|
158
159
|
Ea::Model::Property.new(
|
|
159
|
-
id:
|
|
160
|
+
id: guid,
|
|
161
|
+
association_id: guid,
|
|
160
162
|
name: role,
|
|
161
163
|
owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
|
|
162
164
|
type_name: target&.name,
|
|
@@ -92,9 +92,11 @@ module Ea
|
|
|
92
92
|
model_element_ref: ref_for_object(obj_row),
|
|
93
93
|
bounds: bounds_from_rect(obj_row),
|
|
94
94
|
style: style_hash,
|
|
95
|
-
font_family: style_hash[:font],
|
|
95
|
+
font_family: presence(style_hash[:font]),
|
|
96
|
+
font_size: font_size_from(style_hash),
|
|
96
97
|
font_bold: truthy?(style_hash[:bold]),
|
|
97
98
|
font_italic: truthy?(style_hash[:italic]),
|
|
99
|
+
font_underline: truthy?(style_hash[:ul]),
|
|
98
100
|
show_tagged_values: truthy?(style_hash[:tag])
|
|
99
101
|
)
|
|
100
102
|
end
|
|
@@ -103,6 +105,28 @@ module Ea
|
|
|
103
105
|
raw == "1" || raw == "-1"
|
|
104
106
|
end
|
|
105
107
|
|
|
108
|
+
def presence(value)
|
|
109
|
+
value.nil? || value.to_s.empty? ? nil : value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# EA stores the element font size in tenths of a point in the
|
|
113
|
+
# objectstyle string. 0 means "use default", and 80/90 (8pt /
|
|
114
|
+
# 9pt) are EA's classic defaults the dialogs write back when
|
|
115
|
+
# the user never customized — verified against EA-published
|
|
116
|
+
# reference images, where such elements render at the theme
|
|
117
|
+
# size. Only genuinely custom sizes are surfaced as explicit
|
|
118
|
+
# element fonts.
|
|
119
|
+
USE_DEFAULT_FONTSZ = [0, 80, 90].freeze
|
|
120
|
+
|
|
121
|
+
def font_size_from(style_hash)
|
|
122
|
+
raw = style_hash[:fontsz]
|
|
123
|
+
return nil if raw.nil? || raw.to_s.empty?
|
|
124
|
+
|
|
125
|
+
return nil if USE_DEFAULT_FONTSZ.include?(raw.to_i)
|
|
126
|
+
|
|
127
|
+
(raw.to_f / 10).round
|
|
128
|
+
end
|
|
129
|
+
|
|
106
130
|
def build_connectors(diagram_row)
|
|
107
131
|
links = database.diagram_links_for(diagram_row.diagram_id) || []
|
|
108
132
|
explicit = links.map { |link_row| build_connector(link_row, diagram_row) }
|
|
@@ -216,6 +240,8 @@ module Ea
|
|
|
216
240
|
connector_type: connector&.connector_type,
|
|
217
241
|
direction: connector&.direction,
|
|
218
242
|
waypoints: waypoints_for_link(link_row, diagram_row),
|
|
243
|
+
source_port: connection_port(diagram_row, connector, :source, geom),
|
|
244
|
+
target_port: connection_port(diagram_row, connector, :target, geom),
|
|
219
245
|
label_boxes: geom[:label_boxes] || {},
|
|
220
246
|
style: DiagramStyleParser.parse(link_row.style),
|
|
221
247
|
hidden: hidden?(link_row),
|
|
@@ -340,12 +366,22 @@ module Ea
|
|
|
340
366
|
geom = parse_geometry_fields(link_row.geometry)
|
|
341
367
|
edge_out = geom[:edge] || 0
|
|
342
368
|
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
source_port = connection_port(diagram_row, connector, :source, geom)
|
|
370
|
+
target_port = connection_port(diagram_row, connector, :target, geom)
|
|
371
|
+
# Waypoint points flow through this method as [x, y] arrays;
|
|
372
|
+
# keep the ports in the same shape.
|
|
373
|
+
source_point = source_port ? [source_port.x, source_port.y] :
|
|
374
|
+
element_edge_point(source_placement, :source, edge_out)
|
|
375
|
+
target_point = target_port ? [target_port.x, target_port.y] :
|
|
376
|
+
element_edge_point(target_placement, :target, edge_out)
|
|
345
377
|
|
|
346
378
|
intermediate = intermediate_waypoints(link_row)
|
|
347
379
|
if intermediate.any?
|
|
348
380
|
points = [source_point, *intermediate, target_point]
|
|
381
|
+
elsif source_port || target_port
|
|
382
|
+
# Exact ports are known — the SX/SY bend heuristics in
|
|
383
|
+
# sx_sy_ex_ey_waypoints would double-apply the offsets.
|
|
384
|
+
points = [source_point, target_point]
|
|
349
385
|
else
|
|
350
386
|
points = sx_sy_ex_ey_waypoints(source_point, target_point, geom)
|
|
351
387
|
end
|
|
@@ -355,13 +391,53 @@ module Ea
|
|
|
355
391
|
end
|
|
356
392
|
end
|
|
357
393
|
|
|
394
|
+
# Absolute connection point EA docks the connector's visible
|
|
395
|
+
# endpoint at, derived from the Geometry SX/SY (source) and
|
|
396
|
+
# EX/EY (target) offsets. The offsets are measured inward from
|
|
397
|
+
# the placed object's box edges — the X offset counts from the
|
|
398
|
+
# edge FACING the other end (source: right edge for positive
|
|
399
|
+
# values, left edge for negative; target mirrored), while the
|
|
400
|
+
# Y offset counts from the top edge for positive values and
|
|
401
|
+
# from the bottom edge for negative. Derived and verified
|
|
402
|
+
# against EA's own rendering on the PLATEAU models.
|
|
403
|
+
def connection_port(diagram_row, connector, end_kind, geom)
|
|
404
|
+
return nil unless connector
|
|
405
|
+
|
|
406
|
+
ea_object_id = end_kind == :source ? connector.start_object_id : connector.end_object_id
|
|
407
|
+
placement = ea_object_id &&
|
|
408
|
+
diagram_object_placement(diagram_row.diagram_id, ea_object_id)
|
|
409
|
+
return nil unless placement
|
|
410
|
+
|
|
411
|
+
x_off = geom[end_kind == :source ? :sx : :ex]
|
|
412
|
+
y_off = geom[end_kind == :source ? :sy : :ey]
|
|
413
|
+
return nil if x_off.nil? || y_off.nil?
|
|
414
|
+
|
|
415
|
+
# EA's SX/SY and EX/EY are the offset of the docking line's
|
|
416
|
+
# direction from the element's CENTER (stored math coords,
|
|
417
|
+
# y up): the port is where the ray from the center through
|
|
418
|
+
# center+offset crosses the box border. Verified against
|
|
419
|
+
# EA-published reference SVGs. Bounds are already mirrored
|
|
420
|
+
# to screen-down space, so the offset's Y negates too.
|
|
421
|
+
b = bounds_from_rect(placement)
|
|
422
|
+
center_x = b.x + b.width / 2.0
|
|
423
|
+
center_y = b.y + b.height / 2.0
|
|
424
|
+
dx = x_off.to_f
|
|
425
|
+
dy = -y_off.to_f
|
|
426
|
+
return nil if dx.zero? && dy.zero?
|
|
427
|
+
|
|
428
|
+
tx = dx.zero? ? Float::INFINITY : (dx > 0 ? b.x + b.width - center_x : b.x - center_x) / dx
|
|
429
|
+
ty = dy.zero? ? Float::INFINITY : (dy > 0 ? b.y + b.height - center_y : b.y - center_y) / dy
|
|
430
|
+
t = [tx, ty].min
|
|
431
|
+
Ea::Model::Point.new(x: (center_x + t * dx).round, y: (center_y + t * dy).round)
|
|
432
|
+
end
|
|
433
|
+
|
|
358
434
|
# Parse t_diagramlinks.Path into [[x, y], ...] intermediate
|
|
359
435
|
# waypoints. Format: `x1:y1;x2:y2;...`. Empty string returns [].
|
|
360
436
|
def intermediate_waypoints(link_row)
|
|
361
437
|
raw = link_row.path.to_s
|
|
362
438
|
return [] if raw.empty?
|
|
363
439
|
|
|
364
|
-
raw.scan(/(-?\d+):(-?\d+)/).map { |x, y| [x.to_i, y.to_i] }
|
|
440
|
+
raw.scan(/(-?\d+):(-?\d+)/).map { |x, y| [x.to_i, -y.to_i] }
|
|
365
441
|
end
|
|
366
442
|
|
|
367
443
|
# Fallback when no explicit Path: compute source/target bend
|
|
@@ -369,10 +445,10 @@ module Ea
|
|
|
369
445
|
def sx_sy_ex_ey_waypoints(source_point, target_point, geom)
|
|
370
446
|
points = [source_point]
|
|
371
447
|
if geom[:sx] && geom[:sy] && (geom[:sx].nonzero? || geom[:sy].nonzero?)
|
|
372
|
-
points << [source_point[0] + geom[:sx], source_point[1]
|
|
448
|
+
points << [source_point[0] + geom[:sx], source_point[1] - geom[:sy]]
|
|
373
449
|
end
|
|
374
450
|
if geom[:ex] && geom[:ey] && (geom[:ex].nonzero? || geom[:ey].nonzero?)
|
|
375
|
-
points << [target_point[0] - geom[:ex], target_point[1]
|
|
451
|
+
points << [target_point[0] - geom[:ex], target_point[1] + geom[:ey]]
|
|
376
452
|
end
|
|
377
453
|
points << target_point
|
|
378
454
|
points
|
|
@@ -427,6 +503,10 @@ module Ea
|
|
|
427
503
|
end
|
|
428
504
|
public :parse_label_boxes
|
|
429
505
|
|
|
506
|
+
# Pure helpers exposed for specs and consumers — no database
|
|
507
|
+
# access, safe to call in isolation.
|
|
508
|
+
public :font_size_from, :connection_port
|
|
509
|
+
|
|
430
510
|
# Parse one label box's colon-separated key=value body into
|
|
431
511
|
# a Hash with string keys. Returns nil if OX/OY are absent.
|
|
432
512
|
def parse_label_style(body)
|
|
@@ -509,8 +589,10 @@ module Ea
|
|
|
509
589
|
# EA's t_diagramobjects stores rect with the origin at the
|
|
510
590
|
# bottom-left (math convention) — recttop > rectbottom for
|
|
511
591
|
# valid bounds, and y values are often negative. Convert to
|
|
512
|
-
# screen-space (origin top-left
|
|
513
|
-
#
|
|
592
|
+
# screen-space (origin top-left, y increasing downward) by
|
|
593
|
+
# mirroring: EA's own SVG export draws the model mirrored, so
|
|
594
|
+
# the flipped top edge is -(max(top, bottom)); verified
|
|
595
|
+
# against EA-published reference images (dx=0, dy=-mirror).
|
|
514
596
|
def bounds_from_rect(obj_row)
|
|
515
597
|
left = obj_row.rectleft || 0
|
|
516
598
|
right = obj_row.rectright || 0
|
|
@@ -518,7 +600,7 @@ module Ea
|
|
|
518
600
|
bottom = obj_row.rectbottom || 0
|
|
519
601
|
Ea::Model::Bounds.new(
|
|
520
602
|
x: [left, right].min,
|
|
521
|
-
y: [top, bottom].
|
|
603
|
+
y: -[top, bottom].max,
|
|
522
604
|
width: (right - left).abs,
|
|
523
605
|
height: (bottom - top).abs
|
|
524
606
|
)
|
|
@@ -27,6 +27,7 @@ module Ea
|
|
|
27
27
|
type_name: row.type,
|
|
28
28
|
qualified_name: "#{owner_object.name}::#{row.name}",
|
|
29
29
|
multiplicity_lower: parse_lower(row),
|
|
30
|
+
multiplicity_upper_raw: raw_upper(row),
|
|
30
31
|
multiplicity_upper: parse_upper(row),
|
|
31
32
|
default_value: row.default,
|
|
32
33
|
is_ordered: boolean(row.isordered),
|
|
@@ -42,6 +43,14 @@ module Ea
|
|
|
42
43
|
|
|
43
44
|
private
|
|
44
45
|
|
|
46
|
+
# EA stores some bounds as literal text ("n"); those render
|
|
47
|
+
# verbatim in attribute lines while computed-unbounded bounds
|
|
48
|
+
# render as "*" (verified against EA-published SVGs).
|
|
49
|
+
def raw_upper(row)
|
|
50
|
+
raw = row.upperbound.to_s
|
|
51
|
+
raw.match?(/\A[nN]\z/) ? raw.downcase : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
45
54
|
def parse_lower(row)
|
|
46
55
|
Integer(row.lowerbound || 1)
|
|
47
56
|
rescue StandardError
|
|
@@ -49,10 +58,11 @@ module Ea
|
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
def parse_upper(row)
|
|
52
|
-
|
|
61
|
+
raw = row.upperbound.to_s
|
|
62
|
+
return -1 if raw.match?(/\A[*nN]\z/)
|
|
53
63
|
|
|
54
64
|
begin
|
|
55
|
-
Integer(
|
|
65
|
+
Integer(raw.empty? ? 1 : raw)
|
|
56
66
|
rescue StandardError
|
|
57
67
|
1
|
|
58
68
|
end
|