ea 0.6.33 → 0.6.35

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e63b36fde438e5d088ee091f1a7e9612eb93d379d066d3abaebca6e3495b3d16
4
- data.tar.gz: fbe7073cfddcd4bb861cb7ea238ea436a7f3f5bab5d661e928b2df596bdde92a
3
+ metadata.gz: dd78b24ea3d07ae30617251edd34bab2723f35af5c996b60986bcc87852baed5
4
+ data.tar.gz: 032f878a8b3d21ee10437df152fa93bbabb22453b8172a3073f17c60b1a1ca8d
5
5
  SHA512:
6
- metadata.gz: e1a3392ff7069a1c9287e94997c9d516ac5ee358fb66f76bf27a310615bcaad6e4daeffb135bbda464976e6cccf24e9d40999fabbec64c2f3a863e0bb5b60b3d
7
- data.tar.gz: 55d8d38d9bc7d613f0673863b6f6ace24bad435d1e59b9662fada852bc38248abcacb5f211f466566b668abfcb7a368c129a57e34b58b389d6621a8b5fa13cde
6
+ metadata.gz: ae990b9d896f3b9f82aabdb1285814aa7163b3a202ec070aa5cb42d732e4a4c333e03f07ef4f7d283c71133dad904879328da646052804dfaacbea2d08782bd6
7
+ data.tar.gz: 994415d780dafd15502b5d64ea7e0a8049a1cc9932a94dce2351a76b79f8fecc80a4cbfcd691409d90048d5c9b8485eb857b53e5fa682c5711b84fd34e0e49de
@@ -187,7 +187,11 @@ module Ea
187
187
  # and associations draw as plain source-first lines
188
188
  # (0CABA7D7's gen "M 125 130 L 125 99"; 568CDFC9's
189
189
  # regenerated assocs).
190
- style: conn.connector_type == "Dependency" ? { direct: true } : {},
190
+ style: if conn.connector_type == "Dependency"
191
+ { direct: true, regenerated: true }
192
+ else
193
+ { regenerated: true }
194
+ end,
191
195
  hidden: false,
192
196
  has_geometry_offsets: false
193
197
  )
@@ -59,7 +59,27 @@ module Ea
59
59
  # Element x-extent: logical bounds only (matches EA's canvas
60
60
  # left/right exactly).
61
61
  # Element y-extent: union of logical + image_bounds (image
62
- # extends below bounds for shadow / image padding).
62
+ # extends below bounds for shadow / image padding), plus the
63
+ # "(from Parent)" subtitle extent: EA draws the subtitle 15px
64
+ # below the body and its text descent adds ~4 more, reserving
65
+ # 19px of canvas below subtitle-bearing packages (the +19
66
+ # height family: 05DF5000 Main, Metadata All, package
67
+ # Clients/Suppliers contexts).
68
+ SUBTITLE_CANVAS_EXTRA = 19
69
+
70
+ def subtitle_points
71
+ pts = []
72
+ (diagram.elements || []).each do |e|
73
+ primary = e.bounds || e.image_bounds
74
+ next unless primary
75
+ next unless subtitle_extends_below?(e)
76
+
77
+ pts << [primary.x, primary.y + primary.height +
78
+ SUBTITLE_CANVAS_EXTRA]
79
+ end
80
+ pts
81
+ end
82
+
63
83
  def element_points
64
84
  pts = []
65
85
  (diagram.elements || []).each do |e|
@@ -67,6 +87,10 @@ module Ea
67
87
  if primary
68
88
  pts << [primary.x, primary.y]
69
89
  pts << [primary.x + primary.width, primary.y + primary.height]
90
+ if subtitle_extends_below?(e)
91
+ pts << [primary.x, primary.y + primary.height +
92
+ SUBTITLE_CANVAS_EXTRA]
93
+ end
70
94
  end
71
95
  if e.bounds && e.image_bounds
72
96
  ib = e.image_bounds
@@ -76,6 +100,24 @@ module Ea
76
100
  end
77
101
  pts
78
102
  end
103
+
104
+ # Mirrors Compartment::PackageFromParent's render conditions:
105
+ # the subtitle draws only on ShowForeign diagrams for a
106
+ # placed package whose parent differs from the diagram's
107
+ # package.
108
+ def subtitle_extends_below?(element)
109
+ return false unless model_index
110
+ return false unless diagram.show_foreign
111
+
112
+ pkg = model_index[element.model_element_ref]
113
+ return false unless pkg.is_a?(Ea::Model::Package)
114
+
115
+ parent_id = pkg.parent_id
116
+ return false if parent_id.nil? || parent_id.empty?
117
+ return false if parent_id == diagram.package_id
118
+
119
+ model_index[parent_id].is_a?(Ea::Model::Package)
120
+ end
79
121
  end
80
122
  end
81
123
  end
@@ -42,6 +42,33 @@ module Ea
42
42
  new(min_x: min_x, min_y: min_y, width: width, height: height)
43
43
  end
44
44
 
45
+ # Second-pass canvas over the DRAWN extents captured by the
46
+ # element pass (grown classifiers, autosized packages, plus
47
+ # BoundsCalculator's subtitle reservation). EA sizes the
48
+ # canvas from drawn extents, so diagrams whose lowest/rightmost
49
+ # element grew get a taller/wider canvas than the stored-rect
50
+ # first pass provides.
51
+ def self.from_drawn(diagram, drawn_bounds, model_index: nil)
52
+ base = BoundsCalculator.new(diagram, model_index: model_index)
53
+ pts = []
54
+ drawn_bounds.each_value do |b|
55
+ pts << [b.x, b.y]
56
+ pts << [b.x + b.width, b.y + b.height]
57
+ end
58
+ extra = base.send(:subtitle_points)
59
+ min_x = (pts + extra).map(&:first).min || 0
60
+ min_y = (pts + extra).map(&:last).min || 0
61
+ max_x = (pts + extra).map(&:first).max || 0
62
+ max_y = (pts + extra).map(&:last).max || 0
63
+ return from(diagram, model_index: model_index) if pts.empty?
64
+
65
+ new(min_x: min_x, min_y: min_y,
66
+ width: (max_x - min_x) + BoundsCalculator::INSET_LEFT +
67
+ BoundsCalculator::INSET_RIGHT,
68
+ height: (max_y - min_y) + BoundsCalculator::INSET_TOP +
69
+ BoundsCalculator::INSET_BOTTOM)
70
+ end
71
+
45
72
  def view_box
46
73
  "0 0 #{width} #{height}"
47
74
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Re-docks DIRECT (auto-routed straight, 2-waypoint) connectors
7
+ # against the DRAWN element outlines captured during the element
8
+ # render pass. EA clips connector rays at drawn boxes — grown
9
+ # classifier heights and autosized package bodies — never the
10
+ # stored logical rect (E58034A3: supplier right-edge dock 155
11
+ # drawn vs 121 stored; 0CABA7D7's generalization far end 99
12
+ # grown vs 84 stored).
13
+ module ConnectorRedock
14
+ module_function
15
+
16
+ # Returns replacement waypoint pairs [source_edge, target_edge]
17
+ # for the connector, or nil when the connector must keep its
18
+ # parsed waypoints (not direct, not 2-point, unresolved ends,
19
+ # self-loop, or no drawn bounds for either end).
20
+ def pairs_for(connector, diagram, model_index, bounds_map)
21
+ return nil unless bounds_map
22
+ return nil unless direct?(connector)
23
+
24
+ points = waypoint_pairs(connector)
25
+ return nil unless points.size == 2
26
+
27
+ rel = relationship_for(connector, model_index)
28
+ return nil unless rel
29
+
30
+ source_ref, target_ref = end_refs(rel)
31
+ return nil unless source_ref && target_ref
32
+
33
+ source_bounds = bounds_map[source_ref]
34
+ target_bounds = bounds_map[target_ref]
35
+ return nil unless source_bounds && target_bounds
36
+ return nil if source_ref == target_ref
37
+
38
+ [edge_point(source_bounds, target_bounds),
39
+ edge_point(target_bounds, source_bounds)]
40
+ end
41
+
42
+ def direct?(connector)
43
+ style = connector.style || {}
44
+ %i[direct regenerated].any? { |k| style.key?(k) } ||
45
+ %w[direct regenerated].any? { |k| style.key?(k) }
46
+ end
47
+
48
+ def waypoint_pairs(connector)
49
+ (connector.waypoints || []).filter_map do |wp|
50
+ next unless wp.position
51
+
52
+ [wp.position.x, wp.position.y]
53
+ end
54
+ end
55
+
56
+ def relationship_for(connector, model_index)
57
+ return nil unless connector.relationship_ref && model_index
58
+
59
+ model_index[connector.relationship_ref]
60
+ end
61
+
62
+ # Polymorphic relationship ends, in connector start→end
63
+ # order: Generalization child→parent (specific_id/general_id),
64
+ # Dependency client→supplier (client_id/supplier_id),
65
+ # Association source_id/target_id.
66
+ def end_refs(rel)
67
+ if rel.respond_to?(:specific_id)
68
+ [rel.specific_id, rel.general_id]
69
+ elsif rel.respond_to?(:client_id)
70
+ [rel.client_id, rel.supplier_id]
71
+ else
72
+ [rel.source_id, rel.target_id]
73
+ end
74
+ end
75
+
76
+ # Center-to-center ray clipped at both box outlines.
77
+ def edge_point(bounds, other_bounds)
78
+ cx = bounds.x + bounds.width / 2
79
+ cy = bounds.y + bounds.height / 2
80
+ ocx = other_bounds.x + other_bounds.width / 2
81
+ ocy = other_bounds.y + other_bounds.height / 2
82
+
83
+ dx = ocx - cx
84
+ dy = ocy - cy
85
+
86
+ if dx.abs * bounds.height > dy.abs * bounds.width
87
+ x = dx.positive? ? bounds.x + bounds.width : bounds.x
88
+ y = safe_y(cx, cy, dx, dy, x)
89
+ [x, y]
90
+ else
91
+ y = dy.positive? ? bounds.y + bounds.height : bounds.y
92
+ x = safe_x(cx, cy, dx, dy, y)
93
+ [x, y]
94
+ end
95
+ end
96
+
97
+ def safe_y(cx, cy, dx, dy, x)
98
+ return cy if dx.zero?
99
+
100
+ cy + dy * (x - cx).abs / dx.abs
101
+ end
102
+
103
+ def safe_x(cx, cy, dx, dy, y)
104
+ return cx if dy.zero?
105
+
106
+ cx + dx * (y - cy).abs / dy.abs
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -16,13 +16,17 @@ module Ea
16
16
  class Connectors
17
17
  LINE_STYLE_KEY = :connector_line
18
18
 
19
- attr_reader :diagram, :canvas, :grouped, :stroke_width
19
+ attr_reader :diagram, :canvas, :grouped, :stroke_width, :drawn_bounds,
20
+ :model_index
20
21
 
21
- def initialize(diagram, canvas: nil, grouped: true, stroke_width: 2)
22
+ def initialize(diagram, canvas: nil, grouped: true, stroke_width: 2,
23
+ drawn_bounds: nil, model_index: nil)
22
24
  @diagram = diagram
23
25
  @canvas = canvas
24
26
  @grouped = grouped
25
27
  @stroke_width = stroke_width
28
+ @drawn_bounds = drawn_bounds
29
+ @model_index = model_index
26
30
  end
27
31
 
28
32
  def layers
@@ -55,7 +59,9 @@ module Ea
55
59
  end
56
60
 
57
61
  def path_for(connector)
58
- pts = waypoints_for(connector)
62
+ pts = ConnectorRedock.pairs_for(connector, diagram, model_index,
63
+ drawn_bounds) ||
64
+ waypoints_for(connector)
59
65
  return nil if pts.size < 2
60
66
 
61
67
  # DIRECT (auto-routed straight) connectors: EA writes the
@@ -21,11 +21,25 @@ module Ea
21
21
 
22
22
  def render
23
23
  canvas = Canvas.from(diagram, model_index: model_index)
24
- layers = LayerSequencer.new(diagram, model_index: model_index,
25
- canvas: canvas, frame: frame,
26
- document: document)
27
- .layers
28
- .reject { |s| s.nil? || s.empty? }
24
+ seq = LayerSequencer.new(diagram, model_index: model_index,
25
+ canvas: canvas, frame: frame,
26
+ document: document)
27
+ seq.layers
28
+ # Second pass over the DRAWN extents: grown classifiers and
29
+ # autosized packages can exceed the stored-rect canvas EA
30
+ # sizes from drawn extents. Skip when the first pass already
31
+ # covers the drawn extents (the common case).
32
+ grown = seq.drawn_bounds
33
+ if grown && grown.values.any? &&
34
+ exceeds?(canvas, grown.values)
35
+ canvas = Canvas.from_drawn(diagram, grown,
36
+ model_index: model_index)
37
+ seq = LayerSequencer.new(diagram, model_index: model_index,
38
+ canvas: canvas, frame: frame,
39
+ document: document)
40
+ seq.layers
41
+ end
42
+ layers = seq.layers.reject { |s| s.nil? || s.empty? }
29
43
  image_layer = emit_images
30
44
  layers << image_layer if image_layer
31
45
 
@@ -55,6 +69,16 @@ module Ea
55
69
  %(<g id="images">\n#{fragments.join("\n")}\n</g>)
56
70
  end
57
71
 
72
+ # True when any drawn bound falls outside the stored-rect
73
+ # canvas content area.
74
+ def exceeds?(canvas, bounds)
75
+ bounds.any? do |b|
76
+ b.x < canvas.min_x || b.y < canvas.min_y ||
77
+ b.x + b.width > canvas.min_x + canvas.width ||
78
+ b.y + b.height > canvas.min_y + canvas.height
79
+ end
80
+ end
81
+
58
82
  def render_image(image)
59
83
  svg = Ea::Image::EmfRenderer.render(image.bytes)
60
84
  return nil unless svg
@@ -31,9 +31,17 @@ module Ea
31
31
  # Each element contributes up to 4 groups (shape, header,
32
32
  # divider, attrs) in EA's per-entity layer order.
33
33
  def groups
34
+ @drawn_bounds = {}
34
35
  ordered_elements.flat_map { |e| groups_for(e) }
35
36
  end
36
37
 
38
+ # Final drawn Bounds per model element reference, captured
39
+ # during rendering (grown heights for overflowing classifiers,
40
+ # autosized outlines for packages). Connector emission re-docks
41
+ # direct lines against these — EA clips connector rays at the
42
+ # drawn outlines, never the stored rect.
43
+ attr_reader :drawn_bounds
44
+
37
45
  private
38
46
 
39
47
  def ordered_elements
@@ -46,9 +54,45 @@ module Ea
46
54
  context = build_context(element)
47
55
  return [] unless context
48
56
 
57
+ record_drawn_bounds(element, context)
49
58
  Compartment.render_all(context).compact
50
59
  end
51
60
 
61
+ # Logical-space drawn bounds: stored x/y with the RENDERED
62
+ # height (grown classifiers) and rendered width (autosized
63
+ # packages). Heights/widths are translation-invariant, so the
64
+ # connector emitters can redock in logical space and run the
65
+ # normal canvas translation afterwards.
66
+ def record_drawn_bounds(element, context)
67
+ return unless element.model_element_ref
68
+
69
+ raw = element.bounds || element.image_bounds
70
+ return unless raw
71
+
72
+ width = raw.width
73
+ if classifier_package?(context.classifier)
74
+ tab = Element::PackageShapeRenderer.autosized_tab_width(
75
+ label: context.classifier.name.to_s,
76
+ stereotype: package_stereotype(context),
77
+ size: context.size
78
+ )
79
+ width = tab + Element::PackageShapeRenderer::TAB_BODY_EXTRA
80
+ end
81
+ @drawn_bounds[element.model_element_ref] = Ea::Model::Bounds.new(
82
+ x: raw.x, y: raw.y,
83
+ width: width,
84
+ height: context.bounds.height
85
+ )
86
+ end
87
+
88
+ def classifier_package?(classifier)
89
+ classifier.class == Ea::Model::Package
90
+ end
91
+
92
+ def package_stereotype(context)
93
+ context.classifier.stereotype_refs.first.to_s
94
+ end
95
+
52
96
  # Build the RenderContext for one element. Returns nil when
53
97
  # the element has no usable bounds (skip rendering).
54
98
  def build_context(element)
@@ -42,6 +42,13 @@ module Ea
42
42
  layers
43
43
  end
44
44
 
45
+ # Final drawn Bounds per model element ref (grown heights,
46
+ # autosized packages) captured by the element pass; connector
47
+ # emission re-docks direct lines against these.
48
+ def drawn_bounds
49
+ @elements&.drawn_bounds
50
+ end
51
+
45
52
  private
46
53
 
47
54
  def frame_layers
@@ -49,17 +56,22 @@ module Ea
49
56
  end
50
57
 
51
58
  def element_layers
52
- Elements.new(diagram, model_index: model_index, canvas: canvas,
53
- document: document).groups
59
+ @elements = Elements.new(diagram, model_index: model_index,
60
+ canvas: canvas,
61
+ document: document)
62
+ @elements.groups
54
63
  end
55
64
 
56
65
  def connector_layers
57
66
  connector_layers_raw = Connectors.new(diagram, canvas: canvas,
58
67
  grouped: true,
59
- stroke_width: DEFAULT_STROKE_WIDTH).layers
68
+ stroke_width: DEFAULT_STROKE_WIDTH,
69
+ drawn_bounds: drawn_bounds,
70
+ model_index: model_index).layers
60
71
  marker_layers_raw = Markers.new(diagram, model_index: model_index,
61
72
  canvas: canvas, grouped: true,
62
- stroke_width: DEFAULT_STROKE_WIDTH).layers
73
+ stroke_width: DEFAULT_STROKE_WIDTH,
74
+ drawn_bounds: drawn_bounds).layers
63
75
  merge_layers_by_style(connector_layers_raw + marker_layers_raw).map(&:to_svg)
64
76
  end
65
77
 
@@ -31,14 +31,17 @@ module Ea
31
31
  DEP_ARROW_HALF_BASE = 5
32
32
  DEP_ARROW_HEIGHT = 15
33
33
 
34
- attr_reader :diagram, :model_index, :canvas, :grouped, :stroke_width
34
+ attr_reader :diagram, :model_index, :canvas, :grouped, :stroke_width,
35
+ :drawn_bounds
35
36
 
36
- def initialize(diagram, model_index:, canvas: nil, grouped: true, stroke_width: 2)
37
+ def initialize(diagram, model_index:, canvas: nil, grouped: true,
38
+ stroke_width: 2, drawn_bounds: nil)
37
39
  @diagram = diagram
38
40
  @model_index = model_index
39
41
  @canvas = canvas
40
42
  @grouped = grouped
41
43
  @stroke_width = stroke_width
44
+ @drawn_bounds = drawn_bounds
42
45
  end
43
46
 
44
47
  def layers
@@ -454,6 +457,10 @@ module Ea
454
457
  end
455
458
 
456
459
  def waypoint_pairs(connector)
460
+ redocked = ConnectorRedock.pairs_for(connector, diagram,
461
+ model_index, drawn_bounds)
462
+ return redocked if redocked
463
+
457
464
  (connector.waypoints || []).filter_map do |w|
458
465
  next unless w.position
459
466
 
@@ -45,6 +45,7 @@ module Ea
45
45
  autoload :Document, "ea/svg/ea_emitter/document"
46
46
  autoload :Label, "ea/svg/ea_emitter/label"
47
47
  autoload :Compartment, "ea/svg/ea_emitter/compartment"
48
+ autoload :ConnectorRedock, "ea/svg/ea_emitter/connector_redock"
48
49
  autoload :RenderContext, "ea/svg/ea_emitter/render_context"
49
50
  autoload :VisibilitySymbol, "ea/svg/ea_emitter/visibility_symbol"
50
51
  end
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.33"
7
+ VERSION = "0.6.35"
8
8
  end
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.6.33
4
+ version: 0.6.35
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-10-04 00:00:00.000000000 Z
11
+ date: 2026-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -894,6 +894,7 @@ files:
894
894
  - lib/ea/svg/ea_emitter/compartment/shape.rb
895
895
  - lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb
896
896
  - lib/ea/svg/ea_emitter/compartment/tagged_values.rb
897
+ - lib/ea/svg/ea_emitter/connector_redock.rb
897
898
  - lib/ea/svg/ea_emitter/connectors.rb
898
899
  - lib/ea/svg/ea_emitter/diagram_frame.rb
899
900
  - lib/ea/svg/ea_emitter/document.rb