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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/config/themes/119.yml +4 -4
  4. data/frontend/dist/app.iife.js +8 -8
  5. data/frontend/src/App.vue +6 -2
  6. data/frontend/src/components/AppHeader.vue +35 -8
  7. data/frontend/src/components/AppSidebar.vue +2 -2
  8. data/frontend/src/components/ClassDetails.vue +103 -120
  9. data/frontend/src/components/DiagramView.vue +19 -6
  10. data/frontend/src/components/PackageDetails.vue +67 -26
  11. data/frontend/src/components/PackageTreeNode.vue +25 -20
  12. data/frontend/src/components/SearchResults.vue +21 -8
  13. data/frontend/src/components/WelcomeView.vue +2 -2
  14. data/frontend/src/stores/dataStore.ts +142 -48
  15. data/frontend/src/stores/uiStore.ts +3 -4
  16. data/frontend/src/types.ts +160 -163
  17. data/lib/ea/model/diagram_connector.rb +9 -0
  18. data/lib/ea/model/property.rb +6 -1
  19. data/lib/ea/sources/qea/classifier_builder.rb +3 -1
  20. data/lib/ea/sources/qea/diagram_builder.rb +91 -9
  21. data/lib/ea/sources/qea/property_builder.rb +12 -2
  22. data/lib/ea/spa/output/single_file_strategy.rb +1 -1
  23. data/lib/ea/spa/projector.rb +108 -6
  24. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -0
  25. data/lib/ea/svg/ea_emitter/compartment/constraints.rb +1 -0
  26. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +9 -25
  27. data/lib/ea/svg/ea_emitter/compartment/header.rb +1 -0
  28. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +5 -0
  29. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +51 -11
  30. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +12 -10
  31. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +6 -15
  32. data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +25 -8
  33. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +16 -4
  34. data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +29 -4
  35. data/lib/ea/svg/ea_emitter/elements.rb +70 -20
  36. data/lib/ea/svg/ea_emitter/font_resolver.rb +5 -6
  37. data/lib/ea/svg/ea_emitter/label/end_label.rb +3 -1
  38. data/lib/ea/svg/ea_emitter/labels.rb +38 -2
  39. data/lib/ea/svg/ea_emitter/text_renderer.rb +97 -8
  40. data/lib/ea/svg/parity/checker.rb +19 -9
  41. data/lib/ea/svg/parity/corpus_report.rb +83 -0
  42. data/lib/ea/svg/parity/differ.rb +231 -0
  43. data/lib/ea/svg/parity.rb +2 -0
  44. data/lib/ea/theme/definition.rb +3 -3
  45. data/lib/ea/theme/loader.rb +2 -2
  46. data/lib/ea/version.rb +1 -1
  47. metadata +4 -2
@@ -1,221 +1,218 @@
1
- export interface SpaCardinality {
2
- min?: string
3
- max?: string
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 SpaAttribute {
7
- id: string
8
- name: string
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 SpaAssociationEnd {
22
- class?: string
23
- className?: string
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 SpaAssociation {
30
- id: string
31
- xmiId: string
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 SpaOperation {
40
- id: string
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 SpaParameter {
52
- name: string
53
- type?: string
54
- direction?: string
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 SpaLiteral {
45
+ export interface SpaPackageTreeNode {
46
+ id: string
58
47
  name: string
59
- definition?: string
60
- }
61
-
62
- export interface SpaInheritedAttribute {
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 SpaInheritedAssociation {
71
- associationId: string
72
- inheritedFrom: string
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 SpaClass {
59
+ export interface SpaSkeletonEntry {
79
60
  id: string
80
- xmiId: string
81
61
  name: string
82
- qualifiedName: string
83
- type: string
84
- package?: string
85
- stereotypes: string[]
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 SpaPackage {
68
+ export interface SpaShard {
99
69
  id: string
100
- xmiId: string
101
- name: string
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 SpaTreeClassRef {
74
+ export interface SpaSearchEntry {
112
75
  id: string
76
+ kind: string
113
77
  name: string
114
- stereotypes: string[]
78
+ qualifiedName: string
79
+ package: string
80
+ content: string
81
+ boost?: number
115
82
  }
116
83
 
117
- export interface SpaPackageTreeNode {
84
+ export interface SpaSearchIndex {
85
+ version: string
86
+ fields: string[]
87
+ entries: SpaSearchEntry[]
88
+ }
89
+
90
+ export interface SpaAnnotation {
118
91
  id: string
119
- name: string
120
- path: string
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 SpaDiagram {
96
+ export interface SpaPropertyPayload {
128
97
  id: string
129
- xmiId: string
130
98
  name: string
131
- type: string
132
- package?: string
133
- objectCount: number
134
- linkCount: number
135
- svg?: string
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 SpaStatistics {
139
- packages: number
140
- classes: number
141
- associations: number
142
- attributes: number
143
- operations: number
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 SpaLogoVariant {
147
- path?: string
148
- url?: string
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 SpaLogoConfig {
152
- light: SpaLogoVariant
153
- dark: SpaLogoVariant
134
+ export interface SpaGeneralizationStub {
135
+ id: string
136
+ targetId: string
154
137
  }
155
138
 
156
- export interface SpaLogos {
157
- square?: SpaLogoConfig
158
- long?: SpaLogoConfig
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 SpaAppearance {
162
- logos?: SpaLogos
151
+ export interface SpaEnumerationLiteralPayload {
152
+ id: string
153
+ name: string
154
+ value?: string
155
+ ordinal?: number
163
156
  }
164
157
 
165
- export interface SpaMetadata {
166
- title?: string
167
- description?: string
168
- generated: string
169
- generator: string
170
- version: string
171
- homepage?: string
172
- repository?: string
173
- license?: string
174
- authors?: string
175
- tags?: string[]
176
- appearance?: SpaAppearance
177
- statistics: SpaStatistics
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 SpaSearchEntry {
191
+ export interface SpaDiagramPayload {
181
192
  id: string
182
- type: string
183
- entityType: string
184
- entityId: string
185
193
  name: string
186
- qualifiedName: string
187
- package: string
188
- content: string
189
- boost: number
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 SpaSearchIndex {
193
- version: string
194
- fields: { name: string; boost: number }[]
195
- ref: string
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 SpaDocument {
208
+ export interface SpaSkeleton {
201
209
  metadata: SpaMetadata
202
- packageTree: SpaPackageTreeNode
203
- packages: Record<string, SpaPackage>
204
- classes: Record<string, SpaClass>
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
@@ -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 # -1 means "many"
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: IdNormalizer.from_guid(conn.ea_guid),
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
- source_point = element_edge_point(source_placement, :source, edge_out)
344
- target_point = element_edge_point(target_placement, :target, edge_out)
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] + geom[:sy]]
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] - geom[:ey]]
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) by taking min/max and using
513
- # absolute width/height.
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].min,
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
- return -1 if row.upperbound.to_s == "*"
61
+ raw = row.upperbound.to_s
62
+ return -1 if raw.match?(/\A[*nN]\z/)
53
63
 
54
64
  begin
55
- Integer(row.upperbound || 1)
65
+ Integer(raw.empty? ? 1 : raw)
56
66
  rescue StandardError
57
67
  1
58
68
  end
@@ -19,7 +19,7 @@ module Ea
19
19
  payload = {
20
20
  metadata: skeleton.metadata,
21
21
  packageTree: skeleton.package_tree,
22
- skeletonEntries: skeleton.entries,
22
+ entries: skeleton.entries,
23
23
  searchIndex: projector.search_index,
24
24
  shards: projector.each_shard.to_a
25
25
  }