ea 0.6.139 → 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 +54 -6
- data/lib/ea/model/note.rb +2 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +188 -8
- data/lib/ea/sources/qea/diagram_builder.rb +14 -1
- 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 +58 -15
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/note_builder.rb +2 -1
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +78 -0
- data/lib/ea/sources/xmi/stored_route.rb +83 -6
- 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 +104 -0
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +25 -15
- data/lib/ea/svg/ea_emitter/canvas.rb +80 -14
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +132 -0
- 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 +11 -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 +34 -4
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +30 -5
- 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 +117 -29
- data/lib/ea/svg/ea_emitter/connectors.rb +67 -17
- data/lib/ea/svg/ea_emitter/content_extent.rb +104 -0
- data/lib/ea/svg/ea_emitter/diagram_frame.rb +7 -0
- data/lib/ea/svg/ea_emitter/document.rb +76 -31
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +22 -10
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +57 -32
- data/lib/ea/svg/ea_emitter/elements.rb +24 -5
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +134 -14
- data/lib/ea/svg/ea_emitter/labels.rb +16 -3
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +54 -33
- data/lib/ea/svg/ea_emitter/markers.rb +22 -10
- data/lib/ea/svg/ea_emitter/paint_order.rb +30 -0
- data/lib/ea/svg/ea_emitter/pen.rb +19 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +14 -3
- data/lib/ea/svg/ea_emitter/text_renderer.rb +24 -8
- data/lib/ea/svg/ea_emitter.rb +5 -0
- data/lib/ea/svg/parity/source.rb +1 -1
- data/lib/ea/version.rb +1 -1
- metadata +9 -2
|
@@ -23,26 +23,60 @@ 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
|
-
attr_reader :min_x, :min_y, :width, :height, :inset_left
|
|
29
|
+
attr_reader :min_x, :min_y, :width, :height, :inset_left, :inset_top
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
# `insets` is [left, top]: where model (min_x, min_y) lands on
|
|
32
|
+
# the canvas.
|
|
33
|
+
def initialize(min_x:, min_y:, width:, height:,
|
|
34
|
+
insets: [FRAME_INSET_LEFT, FRAME_INSET_TOP])
|
|
30
35
|
@min_x = min_x
|
|
31
36
|
@min_y = min_y
|
|
32
|
-
@width = width
|
|
33
|
-
@height = height
|
|
34
|
-
@inset_left =
|
|
37
|
+
@width = whole(width)
|
|
38
|
+
@height = whole(height)
|
|
39
|
+
@inset_left, @inset_top = insets
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Model-space origin of this canvas.
|
|
43
|
+
def origin_x
|
|
44
|
+
min_x - inset_left
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def origin_y
|
|
48
|
+
min_y - inset_top
|
|
35
49
|
end
|
|
36
50
|
|
|
37
51
|
# Build a Canvas for a diagram by running the bound
|
|
38
52
|
# calculator. Defaults model_index to nil (no canvas-aware
|
|
39
53
|
# element bounds needed) for callers that only need a
|
|
40
54
|
# frame-size estimate.
|
|
41
|
-
def self.from(diagram, model_index: nil)
|
|
55
|
+
def self.from(diagram, model_index: nil, content_anchor: false)
|
|
42
56
|
bc = BoundsCalculator.new(diagram, model_index: model_index)
|
|
43
57
|
min_x, min_y, width, height = bc.compute
|
|
58
|
+
if content_anchor
|
|
59
|
+
return CanvasAnchor.blank unless bc.max_x
|
|
60
|
+
|
|
61
|
+
return CanvasAnchor.stored(min_x, min_y, bc.max_x, bc.max_y,
|
|
62
|
+
bottom: BottomMargin.for(diagram, model_index))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return empty_framed(diagram, min_x, min_y) unless bc.max_x
|
|
66
|
+
|
|
44
67
|
new(min_x: min_x, min_y: min_y, width: width, height: height,
|
|
45
|
-
|
|
68
|
+
insets: [bc.effective_inset_left || FRAME_INSET_LEFT,
|
|
69
|
+
FRAME_INSET_TOP])
|
|
70
|
+
end
|
|
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])
|
|
46
80
|
end
|
|
47
81
|
|
|
48
82
|
# Second-pass canvas over the DRAWN extents captured by the
|
|
@@ -51,30 +85,56 @@ module Ea
|
|
|
51
85
|
# canvas from drawn extents, so diagrams whose lowest/rightmost
|
|
52
86
|
# element grew get a taller/wider canvas than the stored-rect
|
|
53
87
|
# first pass provides.
|
|
54
|
-
def self.from_drawn(diagram, drawn_bounds, model_index: nil
|
|
88
|
+
def self.from_drawn(diagram, drawn_bounds, model_index: nil,
|
|
89
|
+
content_anchor: false)
|
|
55
90
|
base = BoundsCalculator.new(diagram, model_index: model_index)
|
|
56
91
|
pts = []
|
|
57
92
|
drawn_bounds.each_value do |b|
|
|
58
93
|
pts << [b.x, b.y]
|
|
59
94
|
pts << [b.x + b.width, b.y + b.height]
|
|
60
95
|
end
|
|
61
|
-
extra = base.
|
|
96
|
+
extra = base.subtitle_points
|
|
62
97
|
min_x = (pts + extra).map(&:first).min || 0
|
|
63
98
|
min_y = (pts + extra).map(&:last).min || 0
|
|
64
99
|
max_x = (pts + extra).map(&:first).max || 0
|
|
65
100
|
max_y = (pts + extra).map(&:last).max || 0
|
|
66
|
-
|
|
101
|
+
if pts.empty?
|
|
102
|
+
return from(diagram, model_index: model_index,
|
|
103
|
+
content_anchor: content_anchor)
|
|
104
|
+
end
|
|
105
|
+
if content_anchor
|
|
106
|
+
return CanvasAnchor.stored(min_x, min_y, max_x, max_y,
|
|
107
|
+
bottom: BottomMargin.for(diagram, model_index))
|
|
108
|
+
end
|
|
67
109
|
|
|
68
110
|
# Same scaled rule as BoundsCalculator: drawn leftmost =
|
|
69
111
|
# min(35, 30 + min_x).
|
|
70
|
-
inset_left =
|
|
71
|
-
BoundsCalculator::INSET_LEFT].min
|
|
112
|
+
inset_left = BoundsCalculator.inset_left_for(min_x)
|
|
72
113
|
new(min_x: min_x, min_y: min_y,
|
|
73
114
|
width: (max_x - min_x) + inset_left +
|
|
74
115
|
BoundsCalculator::INSET_RIGHT,
|
|
75
116
|
height: (max_y - min_y) + BoundsCalculator::INSET_TOP +
|
|
76
117
|
BoundsCalculator::INSET_BOTTOM,
|
|
77
|
-
|
|
118
|
+
insets: [inset_left, FRAME_INSET_TOP])
|
|
119
|
+
end
|
|
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])
|
|
78
138
|
end
|
|
79
139
|
|
|
80
140
|
def view_box
|
|
@@ -97,7 +157,7 @@ module Ea
|
|
|
97
157
|
end
|
|
98
158
|
|
|
99
159
|
def translate_y(y)
|
|
100
|
-
y - min_y +
|
|
160
|
+
y - min_y + inset_top
|
|
101
161
|
end
|
|
102
162
|
|
|
103
163
|
# Compact coordinate formatter. Integers lose the decimal
|
|
@@ -111,6 +171,12 @@ module Ea
|
|
|
111
171
|
|
|
112
172
|
private
|
|
113
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
|
+
|
|
114
180
|
def format_cm(px)
|
|
115
181
|
return "0cm" if px.nil? || px.zero?
|
|
116
182
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# Canvas policy of an UNFRAMED export (EA's "Save Diagram Image"
|
|
7
|
+
# without the automatic frame: the PLATEAU native SVGs). Measured
|
|
8
|
+
# on the 220 native SVGs, where it reproduces the origin of 205 of
|
|
9
|
+
# 215 diagrams from the native content alone (the other 10 sit at
|
|
10
|
+
# the floor below):
|
|
11
|
+
#
|
|
12
|
+
# origin = max(min(shape_min - 10, text_left_or_top - 5), -5)
|
|
13
|
+
#
|
|
14
|
+
# in model coordinates, per axis. Shapes are rect/path/polygon/
|
|
15
|
+
# ellipse points; a text box runs from its left edge and from
|
|
16
|
+
# baseline minus the rounded font height. The right margin is 10
|
|
17
|
+
# from the shapes.
|
|
18
|
+
#
|
|
19
|
+
# EMPIRICAL FITS (no saved selector was found in any QEA table):
|
|
20
|
+
# BOTTOM_MARGIN 31 is only the fallback; BottomMargin picks the
|
|
21
|
+
# margin from the lowest element. The right edge also reaches
|
|
22
|
+
# every connector end-label box (its position plus the stored
|
|
23
|
+
# CX width) + LABEL_BOX_MARGIN 5, exact on the 7 PLATEAU diagrams
|
|
24
|
+
# whose labels overhang the shapes (30D62231, 47E4600F,
|
|
25
|
+
# 4881CEEE, 70C77F92, 846DD800, 88AF6BD7, AC2191E3). Below, the
|
|
26
|
+
# same box (its top is the first baseline less the font height,
|
|
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.
|
|
34
|
+
module CanvasAnchor
|
|
35
|
+
CONTENT_MARGIN = 10
|
|
36
|
+
TEXT_MARGIN = 5
|
|
37
|
+
ORIGIN_FLOOR = -5
|
|
38
|
+
BOTTOM_MARGIN = 31
|
|
39
|
+
LABEL_BOX_MARGIN = 5
|
|
40
|
+
# EMPIRICAL FIT: the one PLATEAU diagram with nothing on it
|
|
41
|
+
# (29DD0967) exports a 210 x 110 white canvas.
|
|
42
|
+
BLANK_SIZE = [210, 110].freeze
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Where content starting at `content_min` lands on the canvas.
|
|
47
|
+
def inset(content_min)
|
|
48
|
+
content_min - floored(content_min - CONTENT_MARGIN)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Canvas of a diagram that places no element.
|
|
52
|
+
def blank
|
|
53
|
+
Canvas.new(min_x: 0, min_y: 0, width: BLANK_SIZE[0],
|
|
54
|
+
height: BLANK_SIZE[1], insets: [0, 0])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Canvas for shapes spanning the given model-space extent (the
|
|
58
|
+
# stored element rects, before any drawn content is measured).
|
|
59
|
+
def stored(min_x, min_y, max_x, max_y, bottom: BOTTOM_MARGIN)
|
|
60
|
+
left = inset(min_x)
|
|
61
|
+
top = inset(min_y)
|
|
62
|
+
Canvas.new(min_x: min_x, min_y: min_y,
|
|
63
|
+
width: (max_x - min_x) + left + CONTENT_MARGIN,
|
|
64
|
+
height: (max_y - min_y) + top + bottom,
|
|
65
|
+
insets: [left, top])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Canvas for measured content, each box [min_x, min_y, max_x,
|
|
69
|
+
# max_y] in model coordinates (nil when absent). `label_reach` is
|
|
70
|
+
# the [right, bottom] edge of the connector end-label boxes.
|
|
71
|
+
def measured(shapes, texts, label_reach, bottom: BOTTOM_MARGIN)
|
|
72
|
+
origin_x = origin(shapes && shapes[0], texts && texts[0])
|
|
73
|
+
origin_y = origin(shapes && shapes[1], texts && texts[1])
|
|
74
|
+
Canvas.new(min_x: origin_x, min_y: origin_y,
|
|
75
|
+
width: right_edge(shapes, texts, label_reach) - origin_x,
|
|
76
|
+
height: bottom_edge(shapes, texts, label_reach, bottom) - origin_y,
|
|
77
|
+
insets: [0, 0])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def right_edge(shapes, texts, label_reach)
|
|
81
|
+
[shapes && shapes[2] + CONTENT_MARGIN,
|
|
82
|
+
texts && (texts[2] + TEXT_MARGIN).ceil,
|
|
83
|
+
label_reach && label_reach[0] + LABEL_BOX_MARGIN].compact.max
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def bottom_edge(shapes, texts, label_reach, bottom)
|
|
87
|
+
[shapes[3] + bottom,
|
|
88
|
+
texts && (texts[3] + TEXT_MARGIN).ceil,
|
|
89
|
+
label_reach && label_reach[1] + LABEL_BOX_MARGIN].compact.max
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def origin(shape_min, text_min)
|
|
93
|
+
lead = [shape_min && shape_min - CONTENT_MARGIN,
|
|
94
|
+
text_min && text_min - TEXT_MARGIN].compact.min
|
|
95
|
+
floored(lead)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The floor encodes only "a shape at 0 draws at 5" (the corpus
|
|
99
|
+
# has no shape left of 0). Content starting further out keeps
|
|
100
|
+
# its full margin so nothing is drawn off the canvas.
|
|
101
|
+
def floored(lead)
|
|
102
|
+
lead >= ORIGIN_FLOOR - CONTENT_MARGIN ? [lead, ORIGIN_FLOOR].max : lead
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The canvas anchored on the content drawn into `layers` (the
|
|
106
|
+
# background excluded) and `label_reach`, or `canvas` itself
|
|
107
|
+
# when there is nothing to measure.
|
|
108
|
+
def reanchor(canvas, layers, label_reach, bottom: BOTTOM_MARGIN)
|
|
109
|
+
extent = ContentExtent.measure(layers)
|
|
110
|
+
return canvas unless extent.shapes
|
|
111
|
+
|
|
112
|
+
shift = [canvas.origin_x, canvas.origin_y]
|
|
113
|
+
measured(*[extent.shapes, extent.texts]
|
|
114
|
+
.map { |box| model_box(box, shift) }, label_reach,
|
|
115
|
+
bottom: bottom)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def model_box(box, shift)
|
|
119
|
+
return nil unless box
|
|
120
|
+
|
|
121
|
+
[box[0] + shift[0], box[1] + shift[1],
|
|
122
|
+
box[2] + shift[0], box[3] + shift[1]]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def same_canvas?(one, other)
|
|
126
|
+
[one.origin_x, one.origin_y, one.width, one.height] ==
|
|
127
|
+
[other.origin_x, other.origin_y, other.width, other.height]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -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
|
|
@@ -117,8 +122,12 @@ module Ea
|
|
|
117
122
|
end
|
|
118
123
|
module_function :cjk_char?
|
|
119
124
|
|
|
125
|
+
# The text group keeps the pen the note outline was drawn with
|
|
126
|
+
# (EA never sets a note pen; 7C44AC09 square/miter, 7D960BD8
|
|
127
|
+
# square/bevel), so the next element inherits the right pen.
|
|
120
128
|
def group_style(context)
|
|
121
|
-
|
|
129
|
+
cap, join = pen(context)
|
|
130
|
+
"stroke-width:1;stroke-linecap:#{cap};stroke-linejoin:#{join}; " \
|
|
122
131
|
"fill:#{context.theme.text_color};fill-opacity:1.00; " \
|
|
123
132
|
"stroke:#000000; stroke-opacity:0.00"
|
|
124
133
|
end
|
|
@@ -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)
|
|
@@ -37,9 +42,22 @@ module Ea
|
|
|
37
42
|
|
|
38
43
|
module_function :render
|
|
39
44
|
|
|
40
|
-
# EA draws the subtitle
|
|
41
|
-
#
|
|
42
|
-
|
|
45
|
+
# EA draws the subtitle BELOW the body bottom (outside the
|
|
46
|
+
# box): GAP_BELOW_BODY plus the GDI text ascent of the caption
|
|
47
|
+
# font. Cambria 7pt (H10, ascent 10) gives the 15px seen in
|
|
48
|
+
# every Cambria native package SVG (14E9F5FD, 157826AA,
|
|
49
|
+
# 2A5E0832, 6481E2CC); Carlito 9pt (H13, ascent 12) gives the
|
|
50
|
+
# 17px of 37AC19B9. The 5px gap is fitted on the Cambria cases,
|
|
51
|
+
# the Carlito case is the held-out check. Fonts or units with
|
|
52
|
+
# no GDI cell data keep the legacy 15px.
|
|
53
|
+
# Only families with native evidence use the ascent offset.
|
|
54
|
+
ASCENT_FAMILIES = %w[cambria carlito].freeze
|
|
55
|
+
GAP_BELOW_BODY = 5
|
|
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
|
|
43
61
|
|
|
44
62
|
LINE_PITCH = 13
|
|
45
63
|
WRAP_PADDING = 15
|
|
@@ -47,7 +65,7 @@ module Ea
|
|
|
47
65
|
def subtitle(text, context)
|
|
48
66
|
bounds = context.bounds
|
|
49
67
|
fill = context.theme.attribute_text_color
|
|
50
|
-
y = bounds.y + bounds.height +
|
|
68
|
+
y = bounds.y + bounds.height + offset_from_bottom(context)
|
|
51
69
|
# EA word-wraps the subtitle to the package BODY width
|
|
52
70
|
# (minus padding), centers each line by its integer
|
|
53
71
|
# textLength inside the body, and floors x; successive
|
|
@@ -72,6 +90,18 @@ module Ea
|
|
|
72
90
|
end
|
|
73
91
|
module_function :subtitle
|
|
74
92
|
|
|
93
|
+
def offset_from_bottom(context)
|
|
94
|
+
height = Ea::Fonts::Metrics.source_logical_height(context.logical_height)
|
|
95
|
+
height ||= Ea::Fonts::Metrics.gdi_logical_height(context.size) if context.size_unit == "pt"
|
|
96
|
+
return LEGACY_Y_OFFSET_FROM_BOTTOM unless ASCENT_FAMILIES.include?(context.family.to_s.downcase)
|
|
97
|
+
|
|
98
|
+
return LEGACY_Y_OFFSET_FROM_BOTTOM unless height.nil? || height <= MAX_CAPTION_HEIGHT
|
|
99
|
+
|
|
100
|
+
ascent, = Ea::Fonts::Metrics.gdi_cell_metrics(context.family, height)
|
|
101
|
+
ascent ? GAP_BELOW_BODY + ascent : LEGACY_Y_OFFSET_FROM_BOTTOM
|
|
102
|
+
end
|
|
103
|
+
module_function :offset_from_bottom
|
|
104
|
+
|
|
75
105
|
# EA carries the break space onto the broken line: line 1
|
|
76
106
|
# is "(from ... Metadata - " with the trailing space, and
|
|
77
107
|
# its textLength includes it (05DF5000: tl 146).
|
|
@@ -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(
|
|
@@ -16,7 +29,8 @@ module Ea
|
|
|
16
29
|
legend: context.model_element.legend,
|
|
17
30
|
family: context.family,
|
|
18
31
|
item_font: { size: legend_item_size(context), unit: context.size_unit,
|
|
19
|
-
logical_height: context.logical_height }
|
|
32
|
+
logical_height: context.logical_height },
|
|
33
|
+
pen: context.previous_pen
|
|
20
34
|
)
|
|
21
35
|
elsif context.model_element.is_a?(Ea::Model::Note)
|
|
22
36
|
Element::NoteShapeRenderer.render(
|
|
@@ -24,7 +38,8 @@ module Ea
|
|
|
24
38
|
body: "", fill: context.fill, stroke: context.stroke,
|
|
25
39
|
stroke_width: context.stroke_width,
|
|
26
40
|
family: context.family, size: context.size,
|
|
27
|
-
text_fill: context.text_fill
|
|
41
|
+
text_fill: context.text_fill,
|
|
42
|
+
pen: context.previous_pen
|
|
28
43
|
)
|
|
29
44
|
elsif context.model_element.is_a?(Ea::Model::Package)
|
|
30
45
|
label = package_label(context.model_element)
|
|
@@ -59,12 +74,22 @@ module Ea
|
|
|
59
74
|
return unless element
|
|
60
75
|
return element.font_size if element.font_size&.nonzero?
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
inherited = element.font_size_inherited || element.style&.fetch(:fontsz, nil).to_s == "0"
|
|
64
|
-
context.size if inherited
|
|
77
|
+
context.size if inherits_context_size?(element)
|
|
65
78
|
end
|
|
66
79
|
private_class_method :legend_item_size
|
|
67
80
|
|
|
81
|
+
# Parsed source defaults inherit; unspecified callers keep 9pt.
|
|
82
|
+
# A parsed element (provenance recorded) that resolved no size
|
|
83
|
+
# also inherits: XMI legends carry no fontsz at all, and native
|
|
84
|
+
# EA draws their items at the context size (Cambria 7pt).
|
|
85
|
+
def inherits_context_size?(element)
|
|
86
|
+
return true if element.font_size_inherited
|
|
87
|
+
return true if element.style&.fetch(:fontsz, nil).to_s == "0"
|
|
88
|
+
|
|
89
|
+
!element.font_size_inherited.nil? && element.font_size.nil?
|
|
90
|
+
end
|
|
91
|
+
private_class_method :inherits_context_size?
|
|
92
|
+
|
|
68
93
|
# A Text element with LegendOpts= renders as the legend
|
|
69
94
|
# block rather than a folder silhouette. Dispatch on the
|
|
70
95
|
# presence of the attached Legend payload.
|
|
@@ -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
|