ea 0.6.138 → 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.
- checksums.yaml +4 -4
- data/lib/ea/fonts/metrics.rb +22 -2
- data/lib/ea/model/note.rb +2 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +140 -2
- data/lib/ea/sources/qea/diagram_builder.rb +10 -0
- data/lib/ea/sources/qea/note_builder.rb +7 -1
- data/lib/ea/sources/xmi/diagram_builder/element_borders.rb +27 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +40 -11
- data/lib/ea/sources/xmi/note_builder.rb +2 -1
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +94 -0
- data/lib/ea/sources/xmi/stored_route.rb +121 -8
- data/lib/ea/svg/ea_emitter/bottom_margin.rb +100 -0
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +3 -1
- data/lib/ea/svg/ea_emitter/canvas.rb +37 -9
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +124 -0
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +5 -1
- data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +23 -4
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +17 -5
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +113 -41
- data/lib/ea/svg/ea_emitter/connectors.rb +67 -17
- data/lib/ea/svg/ea_emitter/content_extent.rb +95 -0
- data/lib/ea/svg/ea_emitter/document.rb +56 -27
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +14 -6
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +64 -32
- data/lib/ea/svg/ea_emitter/elements.rb +21 -4
- data/lib/ea/svg/ea_emitter/label/end_label.rb +64 -6
- data/lib/ea/svg/ea_emitter/labels.rb +6 -0
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +40 -26
- data/lib/ea/svg/ea_emitter/markers.rb +22 -10
- data/lib/ea/svg/ea_emitter/paint_order.rb +29 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +14 -3
- data/lib/ea/svg/ea_emitter/segment_geometry.rb +123 -0
- data/lib/ea/svg/ea_emitter.rb +6 -0
- data/lib/ea/version.rb +1 -1
- metadata +8 -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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
52
|
-
"fill:#{
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
369
|
-
# edge
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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)
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if
|
|
99
|
-
merged[
|
|
100
|
-
|
|
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
|
|
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:#
|
|
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
|
-
|
|
64
|
-
|
|
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
|