ea 0.6.139 → 0.6.140

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ea/fonts/metrics.rb +22 -2
  3. data/lib/ea/model/note.rb +2 -0
  4. data/lib/ea/sources/qea/diagram_builder/routing.rb +140 -2
  5. data/lib/ea/sources/qea/diagram_builder.rb +10 -0
  6. data/lib/ea/sources/qea/note_builder.rb +7 -1
  7. data/lib/ea/sources/xmi/diagram_builder/element_borders.rb +27 -0
  8. data/lib/ea/sources/xmi/diagram_builder.rb +37 -10
  9. data/lib/ea/sources/xmi/note_builder.rb +2 -1
  10. data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +94 -0
  11. data/lib/ea/sources/xmi/stored_route.rb +83 -6
  12. data/lib/ea/svg/ea_emitter/bottom_margin.rb +100 -0
  13. data/lib/ea/svg/ea_emitter/bounds_calculator.rb +3 -1
  14. data/lib/ea/svg/ea_emitter/canvas.rb +37 -9
  15. data/lib/ea/svg/ea_emitter/canvas_anchor.rb +124 -0
  16. data/lib/ea/svg/ea_emitter/compartment/note_body.rb +5 -1
  17. data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +23 -4
  18. data/lib/ea/svg/ea_emitter/compartment/shape.rb +17 -5
  19. data/lib/ea/svg/ea_emitter/connector_bridges.rb +99 -25
  20. data/lib/ea/svg/ea_emitter/connectors.rb +67 -17
  21. data/lib/ea/svg/ea_emitter/content_extent.rb +95 -0
  22. data/lib/ea/svg/ea_emitter/document.rb +56 -27
  23. data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +14 -6
  24. data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +64 -32
  25. data/lib/ea/svg/ea_emitter/elements.rb +21 -4
  26. data/lib/ea/svg/ea_emitter/label/end_label.rb +64 -6
  27. data/lib/ea/svg/ea_emitter/labels.rb +6 -0
  28. data/lib/ea/svg/ea_emitter/layer_sequencer.rb +40 -26
  29. data/lib/ea/svg/ea_emitter/markers.rb +22 -10
  30. data/lib/ea/svg/ea_emitter/paint_order.rb +29 -0
  31. data/lib/ea/svg/ea_emitter/render_context.rb +14 -3
  32. data/lib/ea/svg/ea_emitter.rb +4 -0
  33. data/lib/ea/version.rb +1 -1
  34. metadata +7 -1
@@ -9,6 +9,8 @@ module Ea
9
9
  # dog-eared rectangle (top-right corner folded in) with the
10
10
  # note text wrapped inside.
11
11
  #
12
+ # Native EA emits the body and the folded corner as two polygons.
13
+ #
12
14
  # Layout:
13
15
  #
14
16
  # ┌──────────────────┐
@@ -19,15 +21,32 @@ module Ea
19
21
  #
20
22
  class NoteShapeRenderer
21
23
  FOLD_SIZE = 12
24
+ FOLD_FILL = "#DCDCDC"
22
25
  TEXT_X_OFFSET = 5
23
26
  TEXT_Y_OFFSET = 12
24
27
  LINE_HEIGHT = 12
25
28
 
26
- def self.render(bounds, body:, fill:, stroke:, stroke_width:, family:, size:, text_fill:)
27
- new(bounds, body, fill, stroke, stroke_width, family, size, text_fill).to_svg
29
+ # EA never sets the note pen: the outline takes the cap and join
30
+ # left by the previous drawing, or the initial pen (square/miter)
31
+ # when the note is the first thing painted. Checked on all six
32
+ # native notes: 7C44AC09 first (square/miter), 7D960BD8 after
33
+ # square/bevel icons (square/bevel), 676B9C9A, 926AFE38, 9DA6C553
34
+ # and C028B286 after round/bevel shapes (round/bevel).
35
+ INITIAL_PEN = %w[square miter].freeze
36
+ PEN_PATTERN = /stroke-linecap:(\w+);stroke-linejoin:(\w+)/
37
+
38
+ # The [cap, join] a rendered group leaves behind, or nil.
39
+ def self.pen_of(group)
40
+ match = PEN_PATTERN.match(group.to_s)
41
+ match&.captures
28
42
  end
29
43
 
30
- def initialize(bounds, body, fill, stroke, stroke_width, family, size, text_fill)
44
+ def self.render(bounds, body:, fill:, stroke:, stroke_width:, family:, size:, text_fill:, pen: nil)
45
+ new(bounds, body, fill, stroke, stroke_width, family, size, text_fill, pen || INITIAL_PEN).to_svg
46
+ end
47
+
48
+ def initialize(bounds, body, fill, stroke, stroke_width, family, size, text_fill, pen)
49
+ @pen = pen
31
50
  @bounds = bounds
32
51
  @body = body.to_s
33
52
  @fill = fill
@@ -39,7 +58,7 @@ module Ea
39
58
  end
40
59
 
41
60
  def to_svg
42
- %(<g style="#{group_style}">\n #{path_body}\n #{fold_line}\n #{text_block}\n</g>)
61
+ [body_layer, fold_layer, text_layer].reject(&:empty?).join("\n")
43
62
  end
44
63
 
45
64
  private
@@ -47,39 +66,52 @@ module Ea
47
66
  attr_reader :bounds, :body, :fill, :stroke, :stroke_width,
48
67
  :family, :size, :text_fill
49
68
 
50
- def group_style
51
- "stroke-width:#{stroke_width};stroke-linecap:square;stroke-linejoin:bevel; " \
52
- "fill:#{fill};fill-opacity:1.00; stroke:#{stroke}; stroke-opacity:1.00"
69
+ def group_style(layer_fill)
70
+ "stroke-width:#{stroke_width};stroke-linecap:#{@pen[0]};stroke-linejoin:#{@pen[1]}; " \
71
+ "fill:#{layer_fill};fill-opacity:1.00; stroke:#{stroke}; stroke-opacity:1.00"
53
72
  end
54
73
 
55
- # Folded-corner body emitted as a closed <path> matching
56
- # EA's encoding (not <polygon>). EA uses M..L..L..Z with
57
- # stroke-linejoin=bevel for the dog-eared silhouette.
58
- def path_body
59
- x = bounds.x
60
- y = bounds.y
61
- w = bounds.width
62
- h = bounds.height
63
- fold = [FOLD_SIZE, w / 3, h / 3].min
64
- d = "M #{Canvas.coord(x)} #{Canvas.coord(y)} " \
65
- "L #{Canvas.coord(x + w - fold)} #{Canvas.coord(y)} " \
66
- "L #{Canvas.coord(x + w)} #{Canvas.coord(y + fold)} " \
67
- "L #{Canvas.coord(x + w)} #{Canvas.coord(y + h)} " \
68
- "L #{Canvas.coord(x)} #{Canvas.coord(y + h)} " \
69
- "L #{Canvas.coord(x)} #{Canvas.coord(y)} Z"
70
- %(<path d="#{d}" shape-rendering="auto"/>)
74
+ def body_layer
75
+ %(<g style="#{group_style(fill)}">\n #{polygon(body_points)}\n</g>)
76
+ end
77
+
78
+ def fold_layer
79
+ %(<g style="#{group_style(FOLD_FILL)}">\n #{polygon(fold_points)}\n</g>)
80
+ end
81
+
82
+ def text_layer
83
+ text = text_block
84
+ text.empty? ? "" : %(<g style="#{group_style(fill)}">\n #{text}\n</g>)
71
85
  end
72
86
 
73
- # Inner edges of the folded corner; the outer path supplies the diagonal.
74
- def fold_line
75
- x = bounds.x
87
+ # EA encodes the note silhouette as a <polygon> (all six
88
+ # native Note placements in the PLATEAU and ArcGIS corpora),
89
+ # starting at the top-left corner and running down first.
90
+ def body_points
91
+ left, top, right, bottom = edges
92
+ [[left, top], [left, bottom], [right, bottom],
93
+ [right, top + fold_size], [right - fold_size, top]]
94
+ end
95
+
96
+ def edges
97
+ [bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height]
98
+ end
99
+
100
+ # The folded corner is a separate triangle, filled light
101
+ # grey regardless of the note's own fill.
102
+ def fold_points
103
+ r = bounds.x + bounds.width
76
104
  y = bounds.y
77
- w = bounds.width
78
- fold = [FOLD_SIZE, w / 3, bounds.height / 3].min
79
- d = "M #{Canvas.coord(x + w - fold)} #{Canvas.coord(y)} " \
80
- "L #{Canvas.coord(x + w - fold)} #{Canvas.coord(y + fold)} " \
81
- "L #{Canvas.coord(x + w)} #{Canvas.coord(y + fold)}"
82
- %(<path d="#{d}" shape-rendering="auto"/>)
105
+ [[r - fold_size, y], [r - fold_size, y + fold_size], [r, y + fold_size]]
106
+ end
107
+
108
+ def fold_size
109
+ [FOLD_SIZE, bounds.width / 3, bounds.height / 3].min
110
+ end
111
+
112
+ def polygon(points)
113
+ list = points.map { |px, py| "#{Canvas.coord(px)} #{Canvas.coord(py)}" }.join(" ")
114
+ %(<polygon points="#{list}" shape-rendering="auto" style="fill-rule:evenodd;"/>)
83
115
  end
84
116
 
85
117
  def text_block
@@ -33,7 +33,14 @@ module Ea
33
33
  def groups
34
34
  @drawn_bounds = {}
35
35
  @divider_y = {}
36
- ordered_elements.flat_map { |e| groups_for(e) }
36
+ @painted_any = false
37
+ @previous_pen = nil
38
+ ordered_elements.flat_map do |e|
39
+ groups = groups_for(e)
40
+ @painted_any ||= groups.any?
41
+ @previous_pen = Element::NoteShapeRenderer.pen_of(groups.last) || @previous_pen
42
+ groups
43
+ end
37
44
  end
38
45
 
39
46
  # Final drawn Bounds per model element reference, captured
@@ -50,8 +57,10 @@ module Ea
50
57
 
51
58
  private
52
59
 
60
+ # EA paints placements by descending Sequence (the lowest
61
+ # Sequence is on top); equal keys keep their stored order.
53
62
  def ordered_elements
54
- (diagram.elements || []).sort_by { |e| e.z_order || 0 }
63
+ (diagram.elements || []).sort_by.with_index { |e, i| [-(e.z_order || 0), i] }
55
64
  end
56
65
 
57
66
  def groups_for(element)
@@ -300,7 +309,9 @@ module Ea
300
309
  canvas: canvas,
301
310
  diagram: diagram,
302
311
  model_index: model_index,
303
- off_canvas_parent_name: parent_name
312
+ off_canvas_parent_name: parent_name,
313
+ first_painted: !@painted_any,
314
+ previous_pen: @previous_pen
304
315
  )
305
316
  end
306
317
 
@@ -380,10 +391,16 @@ module Ea
380
391
  attr_group_lead: attr_group_lead)
381
392
  end
382
393
 
394
+ # A ghost-led header (italic parent label above the stereotype /
395
+ # name) takes the same compact offsets: EA seats its divider at
396
+ # the last header baseline + 7 (empirical fit, 21 QEA dividers
397
+ # that were +1: 0114FD24 uro::AbstractConstruction etc.; 20 of
398
+ # 21 now exact, no other compared item moved). The tight ghost
399
+ # variant has no corpus case and keeps the default offsets.
383
400
  def compact_header_offsets?(classifier, header_lines, size, **options)
384
401
  classifier.is_a?(Ea::Model::Klass) &&
385
402
  Ea::Fonts::Metrics.compact_compartment_font?(size, family: options[:family], unit: options[:size_unit]) &&
386
- !%i[italic italic_tight].include?(header_lines.dig(0, 1)) &&
403
+ header_lines.dig(0, 1) != :italic_tight &&
387
404
  options.values_at(:logical_height, :marker_count, :enum_literals_count,
388
405
  :divider_offset, :attr_first_offset) == [nil, 0, 0, 8, 7] &&
389
406
  theme.compartments.header_line_offset == 6 && theme.compartments.attr_line_offset == 6
@@ -45,6 +45,15 @@ module Ea
45
45
  @font_unit = font_unit
46
46
  @font_weight = font_weight
47
47
  @font_style = font_style
48
+ @reach = []
49
+ end
50
+
51
+ # [right, bottom] edge, in model coordinates, of the label boxes
52
+ # (position + stored CX/CY) emitted so far; nil before any.
53
+ def reach
54
+ return nil if @reach.empty?
55
+
56
+ [@reach.map(&:first).max, @reach.map(&:last).max]
48
57
  end
49
58
 
50
59
  # Returns an Array of `<text>` SVG strings for one endpoint.
@@ -102,6 +111,10 @@ module Ea
102
111
  stereo = connector_source_dest_stereotype(connector, end_kind)
103
112
  stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
104
113
  direction = leg_direction(end_kind, heading)
114
+ if left_seated_source?(connector, end_kind)
115
+ text_pos, mult_pos = left_seated_positions(anchor, role, mult)
116
+ direction = nil
117
+ end
105
118
  if direction && !(tree && zero_offset?(text_box, mult_box))
106
119
  text_pos = plain_leftward_source_role(connector, direction, anchor, text_box, role) ||
107
120
  plain_vertical_role(connector, direction, anchor, text_box, role) ||
@@ -127,6 +140,7 @@ module Ea
127
140
  directional_multiplicity(:left, anchor, mult_box, mult)
128
141
  end
129
142
  mult_pos = target_multiplicity_seat(connector, heading, anchor, mult_box) ||
143
+ target_diagonal_property_multiplicity(connector, heading, anchor, text_box, mult_box) ||
130
144
  face_multiplicity || target_horizontal_multiplicity(heading, anchor, mult_box, mult) ||
131
145
  mult_pos
132
146
  end
@@ -168,7 +182,8 @@ module Ea
168
182
  end
169
183
 
170
184
  build_texts(role:, mult:, standalone:,
171
- text_pos:, mult_pos:, stereo:, text_box:, stereo_pos:)
185
+ text_pos:, mult_pos:, stereo:, text_box:, stereo_pos:,
186
+ mult_box:)
172
187
  end
173
188
 
174
189
  private
@@ -345,6 +360,24 @@ module Ea
345
360
  end
346
361
  end
347
362
 
363
+ def left_seated_source?(connector, end_kind)
364
+ end_kind == :source && connector.style.to_h[:left_seated_source] == true
365
+ end
366
+
367
+ # EMPIRICAL FIT (5038EEE3 connectors 207 and 212, the only reversed
368
+ # links the builder flags): the source cluster sits left of the
369
+ # anchor like a source leaving left, whichever way the route leaves:
370
+ # role right edge 60px out, 7px above; multiplicity ends 3px short
371
+ # of the anchor, 15px below.
372
+ def left_seated_positions(anchor, role, mult)
373
+ role_width = Ea::Fonts::Metrics.text_length(role.to_s, font_size, family: font_family) ||
374
+ role.to_s.length * 6
375
+ mult_width = Ea::Fonts::Metrics.text_length(mult.to_s, font_size, family: font_family) ||
376
+ mult.to_s.length * 6
377
+ [[anchor[0] - 60 - role_width, anchor[1] - 7],
378
+ [anchor[0] - mult_width - 3, anchor[1] + 15]]
379
+ end
380
+
348
381
  # Stored source-end boxes on a 7pt label whose first leg
349
382
  # leaves straight up or straight right are seated at the box
350
383
  # corner, anchor + (OX, OY) with no sign flip, 3px to its
@@ -365,14 +398,15 @@ module Ea
365
398
  nil
366
399
  end
367
400
 
368
- # Saved bounds seat a source Property role at the stereotype's left
369
- # edge. A centred stored box (ALN=1), or a phantom's synthesized box
401
+ # Saved bounds seat a source or target Property role at the stereotype's
402
+ # left edge (target: +endPoint on 159E9DA0 and 90BCA409, the only
403
+ # corpus targets narrower than the stereotype). A centred stored box (ALN=1), or a phantom's synthesized box
370
404
  # without alignment, centres the shorter role over it; a
371
405
  # left-aligned box keeps both lines on that edge.
372
406
  def bounds_role_centering(connector, end_kind, role, stereo, text_box)
373
- return 0 unless end_kind == :source && stereo && seat_font?
407
+ return 0 unless stereo && seat_font?
374
408
  return 0 unless centred_bounds_box?(connector, text_box)
375
- return 0 unless property_label?(connector, :source)
409
+ return 0 unless property_label?(connector, end_kind)
376
410
 
377
411
  -[stereotype_offset(role, stereo, { "alignment" => 1 }), 0].min
378
412
  end
@@ -634,6 +668,16 @@ module Ea
634
668
  property_role_offsets(:down).first if dy.negative? && dy.abs > dx.abs
635
669
  end
636
670
 
671
+ # With no stored multiplicity box, a Property target on a steep
672
+ # upward diagonal seats it beside the anchor, above the role baseline.
673
+ def target_diagonal_property_multiplicity(connector, heading, anchor, text_box, mult_box)
674
+ return nil if mult_box
675
+ return nil unless diagonal_property_box?(connector, text_box)
676
+ return nil unless heading && heading[1].negative? && heading[1].abs > heading[0].abs
677
+
678
+ [anchor[0] + 8, anchor[1] - 7]
679
+ end
680
+
637
681
  def target_vertical_role(connector, heading, anchor, box, role)
638
682
  return nil unless heading && heading[0].zero? && !heading[1].zero?
639
683
 
@@ -905,9 +949,11 @@ module Ea
905
949
  end
906
950
 
907
951
  def build_texts(role:, mult:, standalone:, text_pos:, mult_pos:,
908
- stereo:, text_box:, stereo_pos: nil)
952
+ stereo:, text_box:, stereo_pos: nil, mult_box: nil)
909
953
  if role && !role.empty? && text_pos
910
954
  x, y = text_pos
955
+ note_reach(text_pos, text_box)
956
+ note_reach(mult_pos, mult_box) if mult && !mult.empty? && mult_pos
911
957
  texts = [text_at(x, y, role)]
912
958
  if stereo
913
959
  stereo_x, stereo_y = stereo_pos || [x + stereotype_offset(role, stereo, text_box),
@@ -919,12 +965,24 @@ module Ea
919
965
  end
920
966
  texts
921
967
  elsif standalone && !standalone.empty? && mult_pos
968
+ note_reach(mult_pos, mult_box)
922
969
  [text_at(*mult_pos, standalone)]
923
970
  else
924
971
  []
925
972
  end
926
973
  end
927
974
 
975
+ # A label box starts at the text's x and at its first baseline
976
+ # less the rounded font height (EA's text top).
977
+ def note_reach(position, box)
978
+ width = box && box["cx"]
979
+ height = box && box["cy"]
980
+ return unless width && height
981
+
982
+ @reach << [position[0] + width,
983
+ position[1] - font_pixel_size.ceil + height]
984
+ end
985
+
928
986
  def umldi_label_position(bounds)
929
987
  [bounds[:x], bounds[:y] + font_pixel_size.ceil]
930
988
  end
@@ -43,6 +43,12 @@ module Ea
43
43
  %(<g style="stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00">\n#{texts.join("\n")}\n</g>)
44
44
  end
45
45
 
46
+ # Right edge of the stored end-label boxes (model coordinates)
47
+ # of the last render, or nil when none carry a width.
48
+ def reach
49
+ end_renderer.reach
50
+ end
51
+
46
52
  private
47
53
 
48
54
  def visible_connectors
@@ -49,8 +49,20 @@ module Ea
49
49
  @elements&.drawn_bounds
50
50
  end
51
51
 
52
+ # Row-slot seating stays off. Elements records each divider in canvas
53
+ # space, so it only matched logical connector ends when the canvas
54
+ # origin was 0 (the content-anchored default) and then moved one end
55
+ # 13 px in all four corpus cases it matched (5D6DB5A0 Aggregation and
56
+ # Generalization, ADC8773C, F46E8F19), none of which EA seats on a
57
+ # row; it never matched an XMI route. Under the framed canvas it was
58
+ # inert, which is the behaviour native parity was measured against.
52
59
  def divider_y_by_ref
53
- @elements&.divider_y
60
+ nil
61
+ end
62
+
63
+ # Right edge of the connector end-label boxes, model coordinates.
64
+ def label_reach
65
+ @labels&.reach
54
66
  end
55
67
 
56
68
  private
@@ -66,46 +78,48 @@ module Ea
66
78
  @elements.groups
67
79
  end
68
80
 
81
+ # EA paints connector by connector: each line is followed by
82
+ # its own heads, and only adjacent same-style groups merge.
69
83
  def connector_layers
70
- connector_layers_raw = Connectors.new(diagram, canvas: canvas,
71
- grouped: true,
72
- stroke_width: DEFAULT_STROKE_WIDTH,
73
- drawn_bounds: drawn_bounds,
74
- divider_y_by_ref: divider_y_by_ref,
75
- model_index: model_index).layers
76
- marker_layers_raw = Markers.new(diagram, model_index: model_index,
77
- canvas: canvas, grouped: true,
78
- stroke_width: DEFAULT_STROKE_WIDTH,
79
- drawn_bounds: drawn_bounds,
80
- divider_y_by_ref: divider_y_by_ref).layers
81
- merge_layers_by_style(connector_layers_raw + marker_layers_raw).map(&:to_svg)
84
+ lines = Connectors.new(diagram, canvas: canvas,
85
+ grouped: true,
86
+ stroke_width: DEFAULT_STROKE_WIDTH,
87
+ drawn_bounds: drawn_bounds,
88
+ divider_y_by_ref: divider_y_by_ref,
89
+ model_index: model_index).connector_layers
90
+ markers = Markers.new(diagram, model_index: model_index,
91
+ canvas: canvas, grouped: true,
92
+ stroke_width: DEFAULT_STROKE_WIDTH,
93
+ drawn_bounds: drawn_bounds,
94
+ divider_y_by_ref: divider_y_by_ref).connector_layers
95
+ by_owner = {}.compare_by_identity
96
+ (lines + markers).each { |owner, layer| (by_owner[owner] ||= []) << layer }
97
+ ordered = PaintOrder.connectors(diagram).flat_map { |connector| by_owner.fetch(connector, []) }
98
+ merge_adjacent_by_style(ordered).map(&:to_svg)
82
99
  end
83
100
 
84
101
  def labels_layer
85
- Labels.new(diagram, canvas: canvas, model_index: model_index,
102
+ @labels = Labels.new(diagram, canvas: canvas, model_index: model_index,
86
103
  theme: theme, document: document,
87
104
  drawn_bounds: drawn_bounds,
88
- divider_y_by_ref: divider_y_by_ref).render
105
+ divider_y_by_ref: divider_y_by_ref)
106
+ @labels.render
89
107
  end
90
108
 
91
109
  def ghost_labels_layer
92
110
  GhostLabels.new(diagram, canvas: canvas).render
93
111
  end
94
112
 
95
- def merge_layers_by_style(layers)
96
- merged = {}
97
- layers.each do |layer|
98
- if merged[layer.style_key]
99
- merged[layer.style_key] = Layer.new(
100
- style_key: layer.style_key,
101
- style: layer.style,
102
- body: "#{merged[layer.style_key].body}\n #{layer.body}"
103
- )
113
+ def merge_adjacent_by_style(layers)
114
+ layers.each_with_object([]) do |layer, merged|
115
+ last = merged.last
116
+ if last && last.style_key == layer.style_key
117
+ merged[-1] = Layer.new(style_key: last.style_key, style: last.style,
118
+ body: "#{last.body}\n #{layer.body}")
104
119
  else
105
- merged[layer.style_key] = layer
120
+ merged << layer
106
121
  end
107
122
  end
108
- merged.values
109
123
  end
110
124
  end
111
125
  end
@@ -52,17 +52,28 @@ module Ea
52
52
  entries = visible_connectors.flat_map { |c| entries_for(c) }
53
53
 
54
54
  if grouped
55
- entries.group_by { |e| [e.style_key, e.stroke_width] }.map do |(key, width), grouped_entries|
55
+ entries.group_by { |e| [e.style_key, e.stroke_width, e.stroke_color] }.map do |(key, width, color), grouped_entries|
56
56
  # EA emits ONE marker per connector regardless of
57
57
  # anchor overlap. Verified against plateau Bridge
58
58
  # diagram: 9 generalizations fanning from the same
59
59
  # parent anchor produce 9 distinct triangle polygons
60
60
  # at the same coordinates. The earlier "dedup by
61
61
  # anchor" hypothesis is disproven by reference SVGs.
62
- marker_layer(key, width, grouped_entries.map(&:body).join("\n "))
62
+ marker_layer(key, width, color, grouped_entries.map(&:body).join("\n "))
63
63
  end
64
64
  else
65
- entries.map { |e| marker_layer(e.style_key, e.stroke_width, e.body) }
65
+ entries.map { |e| marker_layer(e.style_key, e.stroke_width, e.stroke_color, e.body) }
66
+ end
67
+ end
68
+
69
+ # One `[connector, Layer]` pair per marker, in diagram link
70
+ # order, so the sequencer can paint each connector's heads right
71
+ # after its line like EA does.
72
+ def connector_layers
73
+ visible_connectors.flat_map do |connector|
74
+ entries_for(connector).map do |e|
75
+ [connector, marker_layer(e.style_key, e.stroke_width, e.stroke_color, e.body)]
76
+ end
66
77
  end
67
78
  end
68
79
 
@@ -86,7 +97,7 @@ module Ea
86
97
  # drives dedup — EA emits one marker per connector.
87
98
  Entry = Struct.new(:style_key, :body, :anchor,
88
99
  :connector_waypoint_count, :stroke_width,
89
- keyword_init: true) do
100
+ :stroke_color, keyword_init: true) do
90
101
  def hash
91
102
  anchor.hash
92
103
  end
@@ -140,18 +151,18 @@ module Ea
140
151
  connector_line: { fill: "#000000", opacity: "0.00" }
141
152
  }.freeze
142
153
 
143
- def marker_layer(key, width, body)
144
- Layer.new(style_key: Connectors.layer_key(key, width, stroke_width),
145
- style: style_for(key, width), body: body)
154
+ def marker_layer(key, width, color, body)
155
+ Layer.new(style_key: Connectors.layer_key(key, width, stroke_width, color),
156
+ style: style_for(key, width, color), body: body)
146
157
  end
147
158
 
148
- def style_for(key, width = stroke_width)
159
+ def style_for(key, width = stroke_width, color = Connectors::DEFAULT_STROKE)
149
160
  spec = STYLE_MAP[key]
150
161
  return nil unless spec
151
162
 
152
163
  "stroke-width:#{width};stroke-linecap:round;stroke-linejoin:bevel; " \
153
164
  "fill:#{spec[:fill]};fill-opacity:#{spec[:opacity]}; " \
154
- "stroke:#000000; stroke-opacity:1.00"
165
+ "stroke:#{color}; stroke-opacity:1.00"
155
166
  end
156
167
 
157
168
  def visible_connectors
@@ -171,7 +182,8 @@ module Ea
171
182
  body: body,
172
183
  anchor: anchor_for(spec),
173
184
  connector_waypoint_count: waypoint_count,
174
- stroke_width: Connectors.stroke_width_for(connector, stroke_width))
185
+ stroke_width: Connectors.stroke_width_for(connector, stroke_width),
186
+ stroke_color: Connectors.stroke_color_for(connector))
175
187
  end
176
188
  end
177
189
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # EA paints connectors (each line followed by its heads) ordered by the
7
+ # start element's placement row, then the connector id: the
8
+ # `paint_order` key the diagram builders store. Verified without a
9
+ # single inversion over 12785 connector pairs of the QEA corpus.
10
+ # Connectors with an unresolvable key follow the keyed ones, and
11
+ # phantoms (no stored key) come last, each in link order.
12
+ module PaintOrder
13
+ module_function
14
+
15
+ def connectors(diagram)
16
+ (diagram.connectors || []).each_with_index.sort_by do |connector, index|
17
+ order = connector.paint_order
18
+ rank = if order.nil?
19
+ 2
20
+ else
21
+ order.empty? ? 1 : 0
22
+ end
23
+ [rank, order || [], index]
24
+ end.map(&:first)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -19,7 +19,7 @@ module Ea
19
19
  :enum_literals, :tagged_values, :constraints, :marker_lines,
20
20
  :package_content_lines,
21
21
  :geometry, :theme, :canvas, :diagram, :model_index,
22
- :off_canvas_parent_name,
22
+ :off_canvas_parent_name, :first_painted, :previous_pen,
23
23
  keyword_init: true
24
24
  )
25
25
  def note_body
@@ -57,11 +57,22 @@ module Ea
57
57
  # roles; the XMI reads them as ownedAttribute), so a class whose
58
58
  # only properties are association ends has no feature compartment
59
59
  # and no divider (A59065AF urf::Administration).
60
+ #
61
+ # A diagram that suppresses a whole feature kind (Style
62
+ # HideAtts=1 / HideOps=1) has no such compartment to draw, so
63
+ # that kind never earns a divider: 4881CEEE (HideAtts=1 and
64
+ # HideOps=1) draws none over classes that own attributes,
65
+ # 8FD488EF likewise. Per-attribute visibility toggles are a
66
+ # different mechanism and keep the divider (10E3E08A).
60
67
  def hidden_features_present?
61
68
  return false unless classifier.respond_to?(:properties)
62
69
 
63
- classifier.properties.any? { |property| !property.association_id } ||
64
- classifier.operations.any?
70
+ config = diagram&.display_config
71
+ shows_attributes = config.nil? || config.show_attributes?
72
+ shows_operations = config.nil? || config.show_operations?
73
+
74
+ (shows_attributes && classifier.properties.any? { |property| !property.association_id }) ||
75
+ (shows_operations && classifier.operations.any?)
65
76
  end
66
77
  end
67
78
  end
@@ -26,10 +26,14 @@ module Ea
26
26
  #
27
27
  module EaEmitter
28
28
  autoload :Canvas, "ea/svg/ea_emitter/canvas"
29
+ autoload :CanvasAnchor, "ea/svg/ea_emitter/canvas_anchor"
30
+ autoload :BottomMargin, "ea/svg/ea_emitter/bottom_margin"
31
+ autoload :ContentExtent, "ea/svg/ea_emitter/content_extent"
29
32
  autoload :Background, "ea/svg/ea_emitter/background"
30
33
  autoload :BoundsCalculator, "ea/svg/ea_emitter/bounds_calculator"
31
34
  autoload :Style, "ea/svg/ea_emitter/style"
32
35
  autoload :Elements, "ea/svg/ea_emitter/elements"
36
+ autoload :PaintOrder, "ea/svg/ea_emitter/paint_order"
33
37
  autoload :Connectors, "ea/svg/ea_emitter/connectors"
34
38
  autoload :ConnectorBridges, "ea/svg/ea_emitter/connector_bridges"
35
39
  private_constant :ConnectorBridges
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.139"
7
+ VERSION = "0.6.140"
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.139
4
+ version: 0.6.140
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -854,6 +854,7 @@ files:
854
854
  - lib/ea/sources/xmi/diagram_builder.rb
855
855
  - lib/ea/sources/xmi/diagram_builder/association_class_metadata.rb
856
856
  - lib/ea/sources/xmi/diagram_builder/connector_metadata.rb
857
+ - lib/ea/sources/xmi/diagram_builder/element_borders.rb
857
858
  - lib/ea/sources/xmi/diagram_builder/font_settings.rb
858
859
  - lib/ea/sources/xmi/diagram_builder/stereotype_profiles.rb
859
860
  - lib/ea/sources/xmi/diagram_builder/umldi_edges.rb
@@ -875,6 +876,7 @@ files:
875
876
  - lib/ea/sources/xmi/relationship_type_map.rb
876
877
  - lib/ea/sources/xmi/stereotype_builder.rb
877
878
  - lib/ea/sources/xmi/stored_route.rb
879
+ - lib/ea/sources/xmi/stored_route/reversed_routes.rb
878
880
  - lib/ea/sources/xmi/tag_builder.rb
879
881
  - lib/ea/sources/xmi/type_classifier_map.rb
880
882
  - lib/ea/sources/xmi/umldi_keyword_extractor.rb
@@ -898,8 +900,10 @@ files:
898
900
  - lib/ea/svg/connector_router.rb
899
901
  - lib/ea/svg/ea_emitter.rb
900
902
  - lib/ea/svg/ea_emitter/background.rb
903
+ - lib/ea/svg/ea_emitter/bottom_margin.rb
901
904
  - lib/ea/svg/ea_emitter/bounds_calculator.rb
902
905
  - lib/ea/svg/ea_emitter/canvas.rb
906
+ - lib/ea/svg/ea_emitter/canvas_anchor.rb
903
907
  - lib/ea/svg/ea_emitter/color_resolver.rb
904
908
  - lib/ea/svg/ea_emitter/compartment.rb
905
909
  - lib/ea/svg/ea_emitter/compartment/attributes.rb
@@ -922,6 +926,7 @@ files:
922
926
  - lib/ea/svg/ea_emitter/connector_bridges.rb
923
927
  - lib/ea/svg/ea_emitter/connector_redock.rb
924
928
  - lib/ea/svg/ea_emitter/connectors.rb
929
+ - lib/ea/svg/ea_emitter/content_extent.rb
925
930
  - lib/ea/svg/ea_emitter/diagram_frame.rb
926
931
  - lib/ea/svg/ea_emitter/document.rb
927
932
  - lib/ea/svg/ea_emitter/element.rb
@@ -976,6 +981,7 @@ files:
976
981
  - lib/ea/svg/ea_emitter/marker/registry.rb
977
982
  - lib/ea/svg/ea_emitter/markers.rb
978
983
  - lib/ea/svg/ea_emitter/orthogonal_ray.rb
984
+ - lib/ea/svg/ea_emitter/paint_order.rb
979
985
  - lib/ea/svg/ea_emitter/render_context.rb
980
986
  - lib/ea/svg/ea_emitter/segment_geometry.rb
981
987
  - lib/ea/svg/ea_emitter/style.rb