ea 0.6.140 → 0.6.141
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/cli/app.rb +10 -0
- data/lib/ea/cli/command/diagrams.rb +4 -1
- data/lib/ea/cli/command/render.rb +8 -1
- data/lib/ea/cli/command/spa.rb +2 -1
- data/lib/ea/cli/command/svg.rb +9 -6
- data/lib/ea/fonts/metrics.rb +44 -16
- data/lib/ea/sources/qea/diagram_builder/routing.rb +64 -22
- data/lib/ea/sources/qea/diagram_builder.rb +4 -1
- data/lib/ea/sources/xmi/diagram_builder.rb +21 -5
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +10 -26
- data/lib/ea/spa/generator.rb +4 -2
- data/lib/ea/spa/projector.rb +4 -2
- data/lib/ea/svg/ea_emitter/bottom_margin.rb +16 -12
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +22 -14
- data/lib/ea/svg/ea_emitter/canvas.rb +43 -5
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +12 -4
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/header.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/marker.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +7 -2
- data/lib/ea/svg/ea_emitter/compartment/note_text.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/operations.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +12 -1
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +14 -1
- data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +6 -0
- data/lib/ea/svg/ea_emitter/compartment/tagged_values.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment.rb +13 -0
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +18 -4
- data/lib/ea/svg/ea_emitter/content_extent.rb +11 -2
- data/lib/ea/svg/ea_emitter/diagram_frame.rb +7 -0
- data/lib/ea/svg/ea_emitter/document.rb +21 -5
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +18 -14
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +1 -8
- data/lib/ea/svg/ea_emitter/elements.rb +11 -9
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +72 -10
- data/lib/ea/svg/ea_emitter/labels.rb +10 -3
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +14 -7
- data/lib/ea/svg/ea_emitter/paint_order.rb +5 -4
- data/lib/ea/svg/ea_emitter/pen.rb +19 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
- data/lib/ea/svg/ea_emitter/text_renderer.rb +24 -8
- data/lib/ea/svg/ea_emitter.rb +1 -0
- data/lib/ea/svg/parity/source.rb +1 -1
- data/lib/ea/version.rb +1 -1
- metadata +3 -2
|
@@ -33,6 +33,13 @@ module Ea
|
|
|
33
33
|
|
|
34
34
|
attr_reader :diagram, :model_index
|
|
35
35
|
|
|
36
|
+
# Where the leftmost model x lands: 30 + min_x capped at 35. A
|
|
37
|
+
# negative min_x keeps the 30 of min_x 0 (the corpus has none),
|
|
38
|
+
# so content left of the origin is not drawn off the canvas.
|
|
39
|
+
def self.inset_left_for(min_x)
|
|
40
|
+
[[INSET_LEFT - 5 + min_x, INSET_LEFT - 5].max, INSET_LEFT].min
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
def initialize(diagram, model_index: nil)
|
|
37
44
|
@diagram = diagram
|
|
38
45
|
@model_index = model_index
|
|
@@ -53,7 +60,7 @@ module Ea
|
|
|
53
60
|
# Left inset scales from 30 up to the 35 cap with the
|
|
54
61
|
# stored leftmost x: leftmost drawn x = 30 + min_x capped
|
|
55
62
|
# at 35 (corpus: min_x 0 -> 30, 2 -> 32, 4 -> 34, >=5 -> 35).
|
|
56
|
-
@effective_inset_left =
|
|
63
|
+
@effective_inset_left = self.class.inset_left_for(min_x)
|
|
57
64
|
[
|
|
58
65
|
min_x,
|
|
59
66
|
min_y,
|
|
@@ -62,19 +69,7 @@ module Ea
|
|
|
62
69
|
]
|
|
63
70
|
end
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Element x-extent: logical bounds only (matches EA's canvas
|
|
68
|
-
# left/right exactly).
|
|
69
|
-
# Element y-extent: union of logical + image_bounds (image
|
|
70
|
-
# extends below bounds for shadow / image padding), plus the
|
|
71
|
-
# "(from Parent)" subtitle extent: EA draws the subtitle 15px
|
|
72
|
-
# below the body and its text descent adds ~4 more, reserving
|
|
73
|
-
# 19px of canvas below subtitle-bearing packages (the +19
|
|
74
|
-
# height family: 05DF5000 Main, Metadata All, package
|
|
75
|
-
# Clients/Suppliers contexts).
|
|
76
|
-
SUBTITLE_CANVAS_EXTRA = 19
|
|
77
|
-
|
|
72
|
+
# Canvas points reserved below "(from Parent)" subtitles.
|
|
78
73
|
def subtitle_points
|
|
79
74
|
pts = []
|
|
80
75
|
(diagram.elements || []).each do |e|
|
|
@@ -88,6 +83,19 @@ module Ea
|
|
|
88
83
|
pts
|
|
89
84
|
end
|
|
90
85
|
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Element x-extent: logical bounds only (matches EA's canvas
|
|
89
|
+
# left/right exactly).
|
|
90
|
+
# Element y-extent: union of logical + image_bounds (image
|
|
91
|
+
# extends below bounds for shadow / image padding), plus the
|
|
92
|
+
# "(from Parent)" subtitle extent: EA draws the subtitle 15px
|
|
93
|
+
# below the body and its text descent adds ~4 more, reserving
|
|
94
|
+
# 19px of canvas below subtitle-bearing packages (the +19
|
|
95
|
+
# height family: 05DF5000 Main, Metadata All, package
|
|
96
|
+
# Clients/Suppliers contexts).
|
|
97
|
+
SUBTITLE_CANVAS_EXTRA = 19
|
|
98
|
+
|
|
91
99
|
def element_points
|
|
92
100
|
pts = []
|
|
93
101
|
(diagram.elements || []).each do |e|
|
|
@@ -23,6 +23,8 @@ module Ea
|
|
|
23
23
|
FRAME_INSET_LEFT = 35
|
|
24
24
|
INSET_TOUCHING_ORIGIN = 30
|
|
25
25
|
FRAME_INSET_TOP = 40
|
|
26
|
+
# Where the diagram frame's border line sits from the canvas edge.
|
|
27
|
+
FRAME_BORDER_INSET = 6
|
|
26
28
|
|
|
27
29
|
attr_reader :min_x, :min_y, :width, :height, :inset_left, :inset_top
|
|
28
30
|
|
|
@@ -32,8 +34,8 @@ module Ea
|
|
|
32
34
|
insets: [FRAME_INSET_LEFT, FRAME_INSET_TOP])
|
|
33
35
|
@min_x = min_x
|
|
34
36
|
@min_y = min_y
|
|
35
|
-
@width = width
|
|
36
|
-
@height = height
|
|
37
|
+
@width = whole(width)
|
|
38
|
+
@height = whole(height)
|
|
37
39
|
@inset_left, @inset_top = insets
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -60,11 +62,23 @@ module Ea
|
|
|
60
62
|
bottom: BottomMargin.for(diagram, model_index))
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
return empty_framed(diagram, min_x, min_y) unless bc.max_x
|
|
66
|
+
|
|
63
67
|
new(min_x: min_x, min_y: min_y, width: width, height: height,
|
|
64
68
|
insets: [bc.effective_inset_left || FRAME_INSET_LEFT,
|
|
65
69
|
FRAME_INSET_TOP])
|
|
66
70
|
end
|
|
67
71
|
|
|
72
|
+
# No element to size on: the frame alone, wide enough for its
|
|
73
|
+
# name tab.
|
|
74
|
+
def self.empty_framed(diagram, min_x, min_y)
|
|
75
|
+
width = [INSET_TOUCHING_ORIGIN + BoundsCalculator::INSET_RIGHT,
|
|
76
|
+
DiagramFrame.new(canvas: nil).min_width(diagram)].max
|
|
77
|
+
new(min_x: min_x, min_y: min_y, width: width,
|
|
78
|
+
height: BoundsCalculator::INSET_TOP + BoundsCalculator::INSET_BOTTOM,
|
|
79
|
+
insets: [BoundsCalculator.inset_left_for(min_x), FRAME_INSET_TOP])
|
|
80
|
+
end
|
|
81
|
+
|
|
68
82
|
# Second-pass canvas over the DRAWN extents captured by the
|
|
69
83
|
# element pass (grown classifiers, autosized packages, plus
|
|
70
84
|
# BoundsCalculator's subtitle reservation). EA sizes the
|
|
@@ -79,7 +93,7 @@ module Ea
|
|
|
79
93
|
pts << [b.x, b.y]
|
|
80
94
|
pts << [b.x + b.width, b.y + b.height]
|
|
81
95
|
end
|
|
82
|
-
extra = base.
|
|
96
|
+
extra = base.subtitle_points
|
|
83
97
|
min_x = (pts + extra).map(&:first).min || 0
|
|
84
98
|
min_y = (pts + extra).map(&:last).min || 0
|
|
85
99
|
max_x = (pts + extra).map(&:first).max || 0
|
|
@@ -95,8 +109,7 @@ module Ea
|
|
|
95
109
|
|
|
96
110
|
# Same scaled rule as BoundsCalculator: drawn leftmost =
|
|
97
111
|
# min(35, 30 + min_x).
|
|
98
|
-
inset_left =
|
|
99
|
-
BoundsCalculator::INSET_LEFT].min
|
|
112
|
+
inset_left = BoundsCalculator.inset_left_for(min_x)
|
|
100
113
|
new(min_x: min_x, min_y: min_y,
|
|
101
114
|
width: (max_x - min_x) + inset_left +
|
|
102
115
|
BoundsCalculator::INSET_RIGHT,
|
|
@@ -105,6 +118,25 @@ module Ea
|
|
|
105
118
|
insets: [inset_left, FRAME_INSET_TOP])
|
|
106
119
|
end
|
|
107
120
|
|
|
121
|
+
# This canvas when every point of `shapes` ([min_x, min_y, max_x,
|
|
122
|
+
# max_y] in canvas coordinates) lies inside its frame border (the
|
|
123
|
+
# FRAME_BORDER_INSET line), otherwise a canvas grown so the shapes
|
|
124
|
+
# sit CONTENT_MARGIN inside the border on the overflowing sides.
|
|
125
|
+
def holding(shapes)
|
|
126
|
+
left, top, right, bottom = shapes
|
|
127
|
+
reach = CanvasAnchor::CONTENT_MARGIN
|
|
128
|
+
grow_left = left < FRAME_BORDER_INSET ? reach - left : 0
|
|
129
|
+
grow_top = top < FRAME_BORDER_INSET ? reach - top : 0
|
|
130
|
+
new_width = right > width - FRAME_BORDER_INSET ? right + reach : width
|
|
131
|
+
new_height = bottom > height - FRAME_BORDER_INSET ? bottom + reach : height
|
|
132
|
+
return self if [grow_left, grow_top].all?(&:zero?) &&
|
|
133
|
+
new_width == width && new_height == height
|
|
134
|
+
|
|
135
|
+
Canvas.new(min_x: min_x - grow_left, min_y: min_y - grow_top,
|
|
136
|
+
width: new_width + grow_left, height: new_height + grow_top,
|
|
137
|
+
insets: [inset_left, inset_top])
|
|
138
|
+
end
|
|
139
|
+
|
|
108
140
|
def view_box
|
|
109
141
|
"0 0 #{width} #{height}"
|
|
110
142
|
end
|
|
@@ -139,6 +171,12 @@ module Ea
|
|
|
139
171
|
|
|
140
172
|
private
|
|
141
173
|
|
|
174
|
+
# Measured extents arrive as Floats ("508.0"); EA writes whole
|
|
175
|
+
# pixels into the viewBox and background rect.
|
|
176
|
+
def whole(pixels)
|
|
177
|
+
pixels.is_a?(Float) && pixels == pixels.to_i ? pixels.to_i : pixels
|
|
178
|
+
end
|
|
179
|
+
|
|
142
180
|
def format_cm(px)
|
|
143
181
|
return "0cm" if px.nil? || px.zero?
|
|
144
182
|
|
|
@@ -25,6 +25,12 @@ module Ea
|
|
|
25
25
|
# 4881CEEE, 70C77F92, 846DD800, 88AF6BD7, AC2191E3). Below, the
|
|
26
26
|
# same box (its top is the first baseline less the font height,
|
|
27
27
|
# its height the stored CY) + 5 holds the canvas open on 95246DCF.
|
|
28
|
+
#
|
|
29
|
+
# Drawn text also holds the right and bottom edges open (text
|
|
30
|
+
# right/bottom + TEXT_MARGIN, symmetric with the origin rule), so
|
|
31
|
+
# a name wider than its box is not clipped. Checked on the 188
|
|
32
|
+
# PLATEAU natives: widths/heights unchanged (185/188, 188/188);
|
|
33
|
+
# 8DAB169F moves 13 closer; no native contradicts it.
|
|
28
34
|
module CanvasAnchor
|
|
29
35
|
CONTENT_MARGIN = 10
|
|
30
36
|
TEXT_MARGIN = 5
|
|
@@ -66,18 +72,20 @@ module Ea
|
|
|
66
72
|
origin_x = origin(shapes && shapes[0], texts && texts[0])
|
|
67
73
|
origin_y = origin(shapes && shapes[1], texts && texts[1])
|
|
68
74
|
Canvas.new(min_x: origin_x, min_y: origin_y,
|
|
69
|
-
width: right_edge(shapes, label_reach) - origin_x,
|
|
70
|
-
height: bottom_edge(shapes, label_reach, bottom) - origin_y,
|
|
75
|
+
width: right_edge(shapes, texts, label_reach) - origin_x,
|
|
76
|
+
height: bottom_edge(shapes, texts, label_reach, bottom) - origin_y,
|
|
71
77
|
insets: [0, 0])
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
def right_edge(shapes, label_reach)
|
|
80
|
+
def right_edge(shapes, texts, label_reach)
|
|
75
81
|
[shapes && shapes[2] + CONTENT_MARGIN,
|
|
82
|
+
texts && (texts[2] + TEXT_MARGIN).ceil,
|
|
76
83
|
label_reach && label_reach[0] + LABEL_BOX_MARGIN].compact.max
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
def bottom_edge(shapes, label_reach, bottom)
|
|
86
|
+
def bottom_edge(shapes, texts, label_reach, bottom)
|
|
80
87
|
[shapes[3] + bottom,
|
|
88
|
+
texts && (texts[3] + TEXT_MARGIN).ceil,
|
|
81
89
|
label_reach && label_reach[1] + LABEL_BOX_MARGIN].compact.max
|
|
82
90
|
end
|
|
83
91
|
|
|
@@ -6,6 +6,11 @@ module Ea
|
|
|
6
6
|
module Compartment
|
|
7
7
|
# Attribute compartment. Skipped when no attribute lines.
|
|
8
8
|
module Attributes
|
|
9
|
+
# The pen the first group of this compartment leaves behind.
|
|
10
|
+
def self.pen(_context)
|
|
11
|
+
Pen::ROUND_BEVEL
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
module_function
|
|
10
15
|
|
|
11
16
|
def render(context)
|
|
@@ -11,6 +11,11 @@ module Ea
|
|
|
11
11
|
# content and suppresses them when they would overflow the
|
|
12
12
|
# stored box height (never truncates rows).
|
|
13
13
|
module Constraints
|
|
14
|
+
# The pen the first group of this compartment leaves behind.
|
|
15
|
+
def self.pen(_context)
|
|
16
|
+
Pen::ROUND_BEVEL
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
module_function
|
|
15
20
|
|
|
16
21
|
def render(context)
|
|
@@ -8,6 +8,11 @@ module Ea
|
|
|
8
8
|
# italic header + each literal name. Skipped when the
|
|
9
9
|
# classifier has no literals or no content geometry.
|
|
10
10
|
module EnumLiterals
|
|
11
|
+
# The pen the first group of this compartment leaves behind.
|
|
12
|
+
def self.pen(_context)
|
|
13
|
+
Pen::ROUND_BEVEL
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
module_function
|
|
12
17
|
|
|
13
18
|
# Caption and literal rows share the same baseline spacing.
|
|
@@ -7,6 +7,11 @@ module Ea
|
|
|
7
7
|
# Header compartment: stereotype label + class name. Skips
|
|
8
8
|
# rendering when the classifier produces no header lines.
|
|
9
9
|
module Header
|
|
10
|
+
# The pen the first group of this compartment leaves behind.
|
|
11
|
+
def self.pen(_context)
|
|
12
|
+
Pen::ROUND_BEVEL
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
module_function
|
|
11
16
|
|
|
12
17
|
def render(context)
|
|
@@ -8,6 +8,11 @@ module Ea
|
|
|
8
8
|
# compartment (attributes / operations / literals / tagged
|
|
9
9
|
# values). Skipped when there is no content below.
|
|
10
10
|
module HeaderDivider
|
|
11
|
+
# The pen the first group of this compartment leaves behind.
|
|
12
|
+
def self.pen(_context)
|
|
13
|
+
Pen::ROUND_BEVEL
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
module_function
|
|
12
17
|
|
|
13
18
|
def render(context)
|
|
@@ -12,6 +12,11 @@ module Ea
|
|
|
12
12
|
# InstanceSpecification or has neither slots nor a
|
|
13
13
|
# classifier.
|
|
14
14
|
module InstanceSlots
|
|
15
|
+
# The pen the first group of this compartment leaves behind.
|
|
16
|
+
def self.pen(_context)
|
|
17
|
+
Pen::ROUND_BEVEL
|
|
18
|
+
end
|
|
19
|
+
|
|
15
20
|
ROW_OFFSET = 13
|
|
16
21
|
SUBTITLE_OFFSET_FROM_BOTTOM = 15
|
|
17
22
|
|
|
@@ -8,6 +8,11 @@ module Ea
|
|
|
8
8
|
# short horizontal line below each instance name to denote
|
|
9
9
|
# that it is an instance (UML convention).
|
|
10
10
|
module InstanceUnderline
|
|
11
|
+
# The pen the first group of this compartment leaves behind.
|
|
12
|
+
def self.pen(_context)
|
|
13
|
+
Pen::SQUARE_MITER
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
Y_OFFSET = 1
|
|
12
17
|
|
|
13
18
|
module_function
|
|
@@ -10,6 +10,11 @@ module Ea
|
|
|
10
10
|
# Real OCL constraints render trailing, after the content —
|
|
11
11
|
# the two are distinct compartments.
|
|
12
12
|
module Marker
|
|
13
|
+
# The pen the first group of this compartment leaves behind.
|
|
14
|
+
def self.pen(_context)
|
|
15
|
+
Pen::ROUND_BEVEL
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
module_function
|
|
14
19
|
|
|
15
20
|
def render(context)
|
|
@@ -15,6 +15,11 @@ module Ea
|
|
|
15
15
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
|
+
# The text group keeps the pen the note outline was drawn with.
|
|
19
|
+
def pen(context)
|
|
20
|
+
context.previous_pen || Pen::INITIAL
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
def render(context)
|
|
19
24
|
return nil if context.model_element.is_a?(Ea::Model::Note) &&
|
|
20
25
|
context.model_element.legend
|
|
@@ -63,7 +68,7 @@ module Ea
|
|
|
63
68
|
# [pitch, first-baseline offset] for the note font.
|
|
64
69
|
def vertical_layout(context)
|
|
65
70
|
size = context.size
|
|
66
|
-
height = context.logical_height
|
|
71
|
+
height = Ea::Fonts::Metrics.source_logical_height(context.logical_height)
|
|
67
72
|
height ||= Ea::Fonts::Metrics.gdi_logical_height(size) if context.size_unit == "pt"
|
|
68
73
|
ascent, descent = Ea::Fonts::Metrics.gdi_cell_metrics(context.family, height)
|
|
69
74
|
return [size + 7, size + 9] unless ascent
|
|
@@ -121,7 +126,7 @@ module Ea
|
|
|
121
126
|
# (EA never sets a note pen; 7C44AC09 square/miter, 7D960BD8
|
|
122
127
|
# square/bevel), so the next element inherits the right pen.
|
|
123
128
|
def group_style(context)
|
|
124
|
-
cap, join = context
|
|
129
|
+
cap, join = pen(context)
|
|
125
130
|
"stroke-width:1;stroke-linecap:#{cap};stroke-linejoin:#{join}; " \
|
|
126
131
|
"fill:#{context.theme.text_color};fill-opacity:1.00; " \
|
|
127
132
|
"stroke:#000000; stroke-opacity:0.00"
|
|
@@ -10,6 +10,11 @@ module Ea
|
|
|
10
10
|
# Lines are estimated by character width and split at word
|
|
11
11
|
# boundaries to fit the element bounds.
|
|
12
12
|
module NoteText
|
|
13
|
+
# The pen the first group of this compartment leaves behind.
|
|
14
|
+
def self.pen(_context)
|
|
15
|
+
Pen::ROUND_BEVEL
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
X_PADDING = 5
|
|
14
19
|
HAND_DRAW_X_PADDING = 10
|
|
15
20
|
Y_FIRST_OFFSET = 15
|
|
@@ -7,6 +7,11 @@ module Ea
|
|
|
7
7
|
# Operation compartment: divider line + operation signatures.
|
|
8
8
|
# Skipped when no operations or when geometry cannot fit them.
|
|
9
9
|
module Operations
|
|
10
|
+
# The pen the first group of this compartment leaves behind.
|
|
11
|
+
def self.pen(_context)
|
|
12
|
+
Pen::ROUND_BEVEL
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
module_function
|
|
11
16
|
|
|
12
17
|
def render(context)
|
|
@@ -11,6 +11,11 @@ module Ea
|
|
|
11
11
|
# text (Enumeration gets a list-style icon; other classifiers
|
|
12
12
|
# get the "folded paper" silhouette).
|
|
13
13
|
module PackageContents
|
|
14
|
+
# The pen the first group of this compartment leaves behind.
|
|
15
|
+
def self.pen(_context)
|
|
16
|
+
Pen::SQUARE_BEVEL
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
ROW_HEIGHT = 16
|
|
15
20
|
ICON_X_OFFSET = 5
|
|
16
21
|
TEXT_X_OFFSET = 23
|
|
@@ -13,6 +13,11 @@ module Ea
|
|
|
13
13
|
# (379 diagrams, 378 exact): classifiers never render the
|
|
14
14
|
# subtitle, and ShowForeign=0 diagrams render none.
|
|
15
15
|
module PackageFromParent
|
|
16
|
+
# The pen the first group of this compartment leaves behind.
|
|
17
|
+
def self.pen(_context)
|
|
18
|
+
Pen::ROUND_BEVEL
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
module_function
|
|
17
22
|
|
|
18
23
|
def render(context)
|
|
@@ -49,6 +54,10 @@ module Ea
|
|
|
49
54
|
ASCENT_FAMILIES = %w[cambria carlito].freeze
|
|
50
55
|
GAP_BELOW_BODY = 5
|
|
51
56
|
LEGACY_Y_OFFSET_FROM_BOTTOM = 15
|
|
57
|
+
# Heights above this (a 100pt caption is H143) are corrupt
|
|
58
|
+
# input; they keep the legacy offset instead of pushing the
|
|
59
|
+
# caption far off the element.
|
|
60
|
+
MAX_CAPTION_HEIGHT = 60
|
|
52
61
|
|
|
53
62
|
LINE_PITCH = 13
|
|
54
63
|
WRAP_PADDING = 15
|
|
@@ -82,10 +91,12 @@ module Ea
|
|
|
82
91
|
module_function :subtitle
|
|
83
92
|
|
|
84
93
|
def offset_from_bottom(context)
|
|
85
|
-
height = context.logical_height
|
|
94
|
+
height = Ea::Fonts::Metrics.source_logical_height(context.logical_height)
|
|
86
95
|
height ||= Ea::Fonts::Metrics.gdi_logical_height(context.size) if context.size_unit == "pt"
|
|
87
96
|
return LEGACY_Y_OFFSET_FROM_BOTTOM unless ASCENT_FAMILIES.include?(context.family.to_s.downcase)
|
|
88
97
|
|
|
98
|
+
return LEGACY_Y_OFFSET_FROM_BOTTOM unless height.nil? || height <= MAX_CAPTION_HEIGHT
|
|
99
|
+
|
|
89
100
|
ascent, = Ea::Fonts::Metrics.gdi_cell_metrics(context.family, height)
|
|
90
101
|
ascent ? GAP_BELOW_BODY + ascent : LEGACY_Y_OFFSET_FROM_BOTTOM
|
|
91
102
|
end
|
|
@@ -9,6 +9,19 @@ module Ea
|
|
|
9
9
|
module Shape
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
+
# Notes and legends take the pen the previous drawing left; every
|
|
13
|
+
# other shape draws its own round/bevel pen.
|
|
14
|
+
def pen(context)
|
|
15
|
+
return Pen::ROUND_BEVEL unless pen_inheriting?(context)
|
|
16
|
+
|
|
17
|
+
context.previous_pen || Pen::INITIAL
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def pen_inheriting?(context)
|
|
21
|
+
legend?(context) || context.model_element.is_a?(Ea::Model::Note)
|
|
22
|
+
end
|
|
23
|
+
private_class_method :pen_inheriting?
|
|
24
|
+
|
|
12
25
|
def render(context)
|
|
13
26
|
if legend?(context)
|
|
14
27
|
Element::LegendRenderer.render(
|
|
@@ -17,7 +30,7 @@ module Ea
|
|
|
17
30
|
family: context.family,
|
|
18
31
|
item_font: { size: legend_item_size(context), unit: context.size_unit,
|
|
19
32
|
logical_height: context.logical_height },
|
|
20
|
-
|
|
33
|
+
pen: context.previous_pen
|
|
21
34
|
)
|
|
22
35
|
elsif context.model_element.is_a?(Ea::Model::Note)
|
|
23
36
|
Element::NoteShapeRenderer.render(
|
|
@@ -13,6 +13,12 @@ module Ea
|
|
|
13
13
|
# Rendered after the header so the icon sits inside the
|
|
14
14
|
# element body below the stereotype label.
|
|
15
15
|
module StereotypeIcon
|
|
16
|
+
# ShapeScript output sets no stroke pen, so the icon leaves the
|
|
17
|
+
# previous pen in force.
|
|
18
|
+
def self.pen(_context)
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
module_function
|
|
17
23
|
|
|
18
24
|
def render(context)
|
|
@@ -7,6 +7,11 @@ module Ea
|
|
|
7
7
|
# Tagged value compartment. Skipped when no tagged values
|
|
8
8
|
# or when geometry cannot fit them.
|
|
9
9
|
module TaggedValues
|
|
10
|
+
# The pen the first group of this compartment leaves behind.
|
|
11
|
+
def self.pen(_context)
|
|
12
|
+
Pen::ROUND_BEVEL
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
DEFAULT_TEXT_COLOR = "#000000"
|
|
11
16
|
|
|
12
17
|
module_function
|
|
@@ -49,6 +49,10 @@ module Ea
|
|
|
49
49
|
InstanceUnderline
|
|
50
50
|
].freeze
|
|
51
51
|
|
|
52
|
+
# One rendered compartment and the pen its first group leaves behind
|
|
53
|
+
# (nil when it sets none, leaving the previous pen in force).
|
|
54
|
+
Painted = Struct.new(:svg, :pen)
|
|
55
|
+
|
|
52
56
|
module_function
|
|
53
57
|
|
|
54
58
|
# Renders every applicable compartment for the context.
|
|
@@ -57,6 +61,15 @@ module Ea
|
|
|
57
61
|
def render_all(context)
|
|
58
62
|
ALL.map { |compartment| compartment.render(context) }
|
|
59
63
|
end
|
|
64
|
+
|
|
65
|
+
# Like render_all, but each applicable compartment comes with the
|
|
66
|
+
# pen it leaves behind; compartments that do not apply are dropped.
|
|
67
|
+
def paint_all(context)
|
|
68
|
+
ALL.filter_map do |compartment|
|
|
69
|
+
svg = compartment.render(context)
|
|
70
|
+
Painted.new(svg, compartment.pen(context)) if svg
|
|
71
|
+
end
|
|
72
|
+
end
|
|
60
73
|
end
|
|
61
74
|
end
|
|
62
75
|
end
|
|
@@ -21,10 +21,9 @@ module Ea
|
|
|
21
21
|
def path_for(connector, points)
|
|
22
22
|
priority = priority_for(connector)
|
|
23
23
|
return centerline(connector, points) unless priority
|
|
24
|
+
return direct_path(connector, priority, points) if points.size == 2 && direct?(connector)
|
|
24
25
|
|
|
25
|
-
segments =
|
|
26
|
-
crossings_for(connector, priority, first, last)
|
|
27
|
-
end
|
|
26
|
+
segments = bridge_segments(connector, priority, points)
|
|
28
27
|
return centerline(connector, points) if segments.all?(&:empty?)
|
|
29
28
|
|
|
30
29
|
serialize(points, segments)
|
|
@@ -32,6 +31,21 @@ module Ea
|
|
|
32
31
|
|
|
33
32
|
private
|
|
34
33
|
|
|
34
|
+
# Direct lines are written target-first like their centerline, so the
|
|
35
|
+
# bridges are walked in that travel direction (arc sweeps are relative
|
|
36
|
+
# to it) and the target repeat closes the path.
|
|
37
|
+
def direct_path(connector, priority, points)
|
|
38
|
+
travel = points.reverse
|
|
39
|
+
segments = bridge_segments(connector, priority, travel)
|
|
40
|
+
return centerline(connector, points) if segments.all?(&:empty?)
|
|
41
|
+
|
|
42
|
+
"#{serialize(travel, segments)} M #{coordinates(travel.first)}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bridge_segments(connector, priority, points)
|
|
46
|
+
points.each_cons(2).map { |first, last| crossings_for(connector, priority, first, last) }
|
|
47
|
+
end
|
|
48
|
+
|
|
35
49
|
def centerline(connector, points)
|
|
36
50
|
seats = points.map { |point| coordinates(point) }
|
|
37
51
|
# Direct lines retain EA's target-first repeat and source-first model.
|
|
@@ -94,7 +108,7 @@ module Ea
|
|
|
94
108
|
|
|
95
109
|
@routes.select do |partner, _points, order|
|
|
96
110
|
!partner.equal?(connector) && order.is_a?(Array) && order.size == priority.size &&
|
|
97
|
-
!same_source?(connector, partner) && (order <=> priority)
|
|
111
|
+
!same_source?(connector, partner) && (order <=> priority)&.negative?
|
|
98
112
|
end
|
|
99
113
|
end
|
|
100
114
|
|
|
@@ -60,13 +60,22 @@ module Ea
|
|
|
60
60
|
path_data.scan(PATH_COMMAND).flat_map do |command, body|
|
|
61
61
|
numbers = body.scan(NUMBER).map(&:to_f)
|
|
62
62
|
case command
|
|
63
|
-
when "M", "L"
|
|
64
|
-
when "
|
|
63
|
+
when "M", "L" then numbers.each_slice(2).select { |pair| pair.size == 2 }
|
|
64
|
+
when "C" then end_points(numbers, 6)
|
|
65
|
+
when "A" then end_points(numbers, 7)
|
|
65
66
|
else []
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
# The segment end point only: a Bezier's control points lie
|
|
72
|
+
# outside the drawn curve and EA does not count them (basic
|
|
73
|
+
# E325B9D8, hand-drawn style: 504 x 274 natively, 508 x 278
|
|
74
|
+
# with the control points).
|
|
75
|
+
def self.end_points(numbers, stride)
|
|
76
|
+
numbers.each_slice(stride).select { |a| a.size == stride }.map { |a| a.last(2) }
|
|
77
|
+
end
|
|
78
|
+
|
|
70
79
|
def self.text_box(attrs)
|
|
71
80
|
style = attrs["style"].to_s
|
|
72
81
|
size = style[/font-size:([\d.]+)/, 1].to_f
|
|
@@ -48,6 +48,13 @@ module Ea
|
|
|
48
48
|
].compact
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Narrowest canvas that holds the name tab inside the border
|
|
52
|
+
# (the border inset on both sides of the tab).
|
|
53
|
+
def min_width(diagram)
|
|
54
|
+
2 * theme.frame.inset + tab_label_width(diagram) +
|
|
55
|
+
theme.frame.tab_slant
|
|
56
|
+
end
|
|
57
|
+
|
|
51
58
|
private
|
|
52
59
|
|
|
53
60
|
def border_layer(canvas)
|
|
@@ -44,10 +44,25 @@ module Ea
|
|
|
44
44
|
content_anchor: content_anchor)
|
|
45
45
|
seq = sequence(canvas)
|
|
46
46
|
canvas, seq = redo_on_drawn(canvas, seq)
|
|
47
|
-
|
|
47
|
+
return anchor_on_content(canvas, seq) if content_anchor
|
|
48
|
+
|
|
49
|
+
frame ? grow_to_drawn_shapes(canvas, seq) : [canvas, seq]
|
|
48
50
|
end
|
|
49
51
|
private :lay_out
|
|
50
52
|
|
|
53
|
+
# A framed canvas is sized from element rects, so a stored
|
|
54
|
+
# connector bend beyond them is drawn outside the viewBox. Grows
|
|
55
|
+
# the canvas to hold every drawn shape; a diagram whose shapes
|
|
56
|
+
# already fit keeps its canvas untouched.
|
|
57
|
+
def grow_to_drawn_shapes(canvas, seq)
|
|
58
|
+
shapes = ContentExtent.measure(seq.layers.drop(1)).shapes
|
|
59
|
+
return [canvas, seq] unless shapes
|
|
60
|
+
|
|
61
|
+
grown = canvas.holding(shapes)
|
|
62
|
+
grown.equal?(canvas) ? [canvas, seq] : [grown, sequence(grown)]
|
|
63
|
+
end
|
|
64
|
+
private :grow_to_drawn_shapes
|
|
65
|
+
|
|
51
66
|
def redo_on_drawn(canvas, seq)
|
|
52
67
|
grown = seq.drawn_bounds
|
|
53
68
|
return [canvas, seq] unless grown&.values&.any? &&
|
|
@@ -103,13 +118,14 @@ module Ea
|
|
|
103
118
|
%(<g id="images">\n#{fragments.join("\n")}\n</g>)
|
|
104
119
|
end
|
|
105
120
|
|
|
106
|
-
# True when any drawn bound falls outside the
|
|
107
|
-
# canvas
|
|
121
|
+
# True when any drawn bound falls outside the canvas content
|
|
122
|
+
# area; the far edges are measured from the canvas' model-space
|
|
123
|
+
# origin (its insets included).
|
|
108
124
|
def exceeds?(canvas, bounds)
|
|
109
125
|
bounds.any? do |b|
|
|
110
126
|
b.x < canvas.min_x || b.y < canvas.min_y ||
|
|
111
|
-
b.x + b.width > canvas.
|
|
112
|
-
b.y + b.height > canvas.
|
|
127
|
+
b.x + b.width > canvas.origin_x + canvas.width ||
|
|
128
|
+
b.y + b.height > canvas.origin_y + canvas.height
|
|
113
129
|
end
|
|
114
130
|
end
|
|
115
131
|
|