ea 0.6.140 → 0.6.142
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/config/themes/default.yml +7 -0
- 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/model/diagram.rb +6 -0
- data/lib/ea/qea/services/database_loader.rb +4 -1
- 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/adapter.rb +20 -0
- data/lib/ea/sources/xmi/connector_index.rb +30 -0
- data/lib/ea/sources/xmi/constraint_builder.rb +81 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +22 -11
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/extension_types.rb +9 -2
- data/lib/ea/sources/xmi/relationship_builder.rb +17 -11
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +10 -26
- data/lib/ea/sources/xmi.rb +2 -0
- 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 +8 -1
- data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +6 -1
- data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +6 -1
- 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/connector_redock.rb +13 -6
- 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 +22 -6
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +6 -1
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +9 -0
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +7 -0
- 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 +36 -12
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +113 -19
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +66 -0
- data/lib/ea/svg/ea_emitter/label/plain_source_seat.rb +25 -2
- data/lib/ea/svg/ea_emitter/label/plain_target_seat.rb +64 -1
- data/lib/ea/svg/ea_emitter/labels.rb +64 -35
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +14 -7
- data/lib/ea/svg/ea_emitter/markers.rb +1 -1
- 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 +5 -2
|
@@ -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
|
|
|
@@ -632,12 +632,19 @@ module Ea
|
|
|
632
632
|
# Dependency client→supplier (client_id/supplier_id),
|
|
633
633
|
# Association source_id/target_id.
|
|
634
634
|
def end_refs(rel)
|
|
635
|
-
case rel
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
635
|
+
refs = case rel
|
|
636
|
+
when Ea::Model::Generalization then [rel.specific_id, rel.general_id]
|
|
637
|
+
when Ea::Model::Realization then [rel.realizing_id, rel.contract_id]
|
|
638
|
+
when Ea::Model::Dependency then [rel.client_id, rel.supplier_id]
|
|
639
|
+
else [rel.source_id, rel.target_id]
|
|
640
|
+
end
|
|
641
|
+
refs.map { |ref| diagram_ref(ref) }
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# A package is `EAPK_<guid>` in the XMI model but is placed on a
|
|
645
|
+
# diagram as `EAID_<guid>`; key its ends by the diagram spelling.
|
|
646
|
+
def diagram_ref(ref)
|
|
647
|
+
ref.is_a?(String) && ref.start_with?("EAPK_") ? "EAID_#{ref[5..]}" : ref
|
|
641
648
|
end
|
|
642
649
|
|
|
643
650
|
# Center-to-center ray clipped at both box outlines.
|
|
@@ -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)
|
|
@@ -17,7 +17,7 @@ module Ea
|
|
|
17
17
|
# origins the canvas on the drawn content like EA's native
|
|
18
18
|
# unframed export; off, it keeps the framed 35/40 content origin.
|
|
19
19
|
def initialize(diagram, model_index:, frame: false, document: nil,
|
|
20
|
-
content_anchor: true
|
|
20
|
+
content_anchor: true)
|
|
21
21
|
@diagram = diagram
|
|
22
22
|
@model_index = model_index
|
|
23
23
|
@frame = frame
|
|
@@ -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
|
|
|
@@ -116,7 +116,12 @@ module Ea
|
|
|
116
116
|
lookup: lookup).to_s
|
|
117
117
|
end
|
|
118
118
|
else
|
|
119
|
-
|
|
119
|
+
# EMPIRICAL FIT (test B58D1A53, the only corpus class mixing
|
|
120
|
+
# stereotyped and plain attributes): EA draws the plain rows
|
|
121
|
+
# first, then the stereotyped ones in stored order.
|
|
122
|
+
plain, stereotyped = ordered_properties(props, stored_order)
|
|
123
|
+
.partition { |prop| property_stereotype(prop).nil? }
|
|
124
|
+
(plain + stereotyped).flat_map do |prop|
|
|
120
125
|
lines = []
|
|
121
126
|
stereotype = property_stereotype(prop)
|
|
122
127
|
lines << "«#{stereotype}»" if stereotype
|
|
@@ -206,6 +206,15 @@ module Ea
|
|
|
206
206
|
content_bottom_y ? content_bottom_y + size + 5 : raw_attr_first_y
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
+
# Bottom edge of the tags compartment: the last tag row plus one
|
|
210
|
+
# row of trailing space (204F4ACC: last baseline 83, EA bottom 95).
|
|
211
|
+
def tagged_values_bottom_y
|
|
212
|
+
return nil unless tagged_value_first_y
|
|
213
|
+
|
|
214
|
+
tagged_value_first_y +
|
|
215
|
+
(tagged_values_count.to_i + 1) * line_height(@size_unit == "pt" ? 6 : 4)
|
|
216
|
+
end
|
|
217
|
+
|
|
209
218
|
# The lowest content edge among attributes, enum literals,
|
|
210
219
|
# operations, and constraints — the anchor for trailing
|
|
211
220
|
# compartments and the autosize height.
|
|
@@ -11,6 +11,7 @@ module Ea
|
|
|
11
11
|
# the left edge from text-width approximation since SVG text
|
|
12
12
|
# x= is the start position, not the center).
|
|
13
13
|
class HeaderRenderer
|
|
14
|
+
OVERFLOW_INSET = 2
|
|
14
15
|
HeaderLine = Struct.new(:text, :style, :x, :y, :text_length,
|
|
15
16
|
:weight, :font_style, keyword_init: true)
|
|
16
17
|
|
|
@@ -83,7 +84,13 @@ module Ea
|
|
|
83
84
|
|
|
84
85
|
# EA centers text by its INTEGER textLength and floors the
|
|
85
86
|
# resulting x (verified exact against EA-published SVGs).
|
|
87
|
+
#
|
|
88
|
+
# EMPIRICAL FIT: a name wider than its box starts 2px inside the
|
|
89
|
+
# left edge instead of centring past it (simple 204F4ACC
|
|
90
|
+
# AcmeUmlEnumeration, 106 in a 90 box: x = box x + 2).
|
|
86
91
|
def self.center_x_for(text_length, bounds)
|
|
92
|
+
return bounds.x + OVERFLOW_INSET if text_length > bounds.width
|
|
93
|
+
|
|
87
94
|
(bounds.x + (bounds.width - text_length) / 2.0).floor
|
|
88
95
|
end
|
|
89
96
|
private_class_method :center_x_for
|
|
@@ -32,23 +32,24 @@ module Ea
|
|
|
32
32
|
LABEL_BASELINE_FROM_ICON_TOP = 12
|
|
33
33
|
DEFAULT_ITEM_FONT_SIZE = 9
|
|
34
34
|
# The legend pen is not set explicitly: the panel inherits the pen
|
|
35
|
-
# left by the previous drawing
|
|
36
|
-
#
|
|
37
|
-
# across the corpus: 11 of 11 legends painted first are
|
|
38
|
-
# of 105 painted later
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
# left by the previous drawing, or EA's initial pen (square/miter)
|
|
36
|
+
# when nothing was drawn before it - the same rule notes follow.
|
|
37
|
+
# Held across the corpus: 11 of 11 legends painted first are
|
|
38
|
+
# square/miter, 105 of 105 painted later follow a class-like
|
|
39
|
+
# drawing that leaves round/bevel.
|
|
40
|
+
INITIAL_PEN = Element::NoteShapeRenderer::INITIAL_PEN
|
|
41
41
|
|
|
42
|
-
def self.render(bounds, legend:, family:, canvas: nil, item_font: {},
|
|
43
|
-
new(bounds, legend, family, canvas, item_font,
|
|
42
|
+
def self.render(bounds, legend:, family:, canvas: nil, item_font: {}, pen: nil)
|
|
43
|
+
new(bounds, legend, family, canvas, item_font, pen: pen).to_svg
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def initialize(bounds, legend, family, canvas, item_font = {},
|
|
46
|
+
def initialize(bounds, legend, family, canvas, item_font = {}, pen: nil)
|
|
47
47
|
@bounds = bounds
|
|
48
48
|
@legend = legend
|
|
49
49
|
@family = family
|
|
50
50
|
@canvas = canvas
|
|
51
|
-
|
|
51
|
+
cap, join = pen || INITIAL_PEN
|
|
52
|
+
@pen = "stroke-width:1;stroke-linecap:#{cap};stroke-linejoin:#{join};"
|
|
52
53
|
@item_font = item_font[:size]&.nonzero? ? item_font : {}
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -162,10 +163,11 @@ module Ea
|
|
|
162
163
|
(block_left + (block_right - block_left - title_text_width.round) / 2.0).floor
|
|
163
164
|
end
|
|
164
165
|
|
|
165
|
-
def estimated_width(text, size, weight: nil, logical_height: nil, text_family: family
|
|
166
|
+
def estimated_width(text, size, weight: nil, logical_height: nil, text_family: family,
|
|
167
|
+
legend_title: false)
|
|
166
168
|
TextRenderer.estimate_width(
|
|
167
169
|
text, size, family: text_family, weight: weight,
|
|
168
|
-
logical_height: logical_height
|
|
170
|
+
logical_height: logical_height, legend_title: legend_title
|
|
169
171
|
).round
|
|
170
172
|
end
|
|
171
173
|
|
|
@@ -193,7 +195,8 @@ module Ea
|
|
|
193
195
|
end
|
|
194
196
|
|
|
195
197
|
def title_text_width
|
|
196
|
-
estimated_width(legend.title, title_font_size, weight: 700, text_family: heading_family
|
|
198
|
+
estimated_width(legend.title, title_font_size, weight: 700, text_family: heading_family,
|
|
199
|
+
legend_title: true)
|
|
197
200
|
end
|
|
198
201
|
|
|
199
202
|
def heading_family
|
|
@@ -206,7 +209,8 @@ module Ea
|
|
|
206
209
|
TextRenderer.new(
|
|
207
210
|
content: content.to_s, x: tx, y: ty,
|
|
208
211
|
family: weight.zero? ? family : heading_family, size: size, size_unit: size_unit,
|
|
209
|
-
weight: weight, fill: label_fill, logical_height: weight.zero? ? item_font[:logical_height] : nil
|
|
212
|
+
weight: weight, fill: label_fill, logical_height: weight.zero? ? item_font[:logical_height] : nil,
|
|
213
|
+
legend_title: !weight.zero?
|
|
210
214
|
).to_svg
|
|
211
215
|
end
|
|
212
216
|
|
|
@@ -32,14 +32,7 @@ module Ea
|
|
|
32
32
|
# native notes: 7C44AC09 first (square/miter), 7D960BD8 after
|
|
33
33
|
# square/bevel icons (square/bevel), 676B9C9A, 926AFE38, 9DA6C553
|
|
34
34
|
# and C028B286 after round/bevel shapes (round/bevel).
|
|
35
|
-
INITIAL_PEN =
|
|
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
|
|
42
|
-
end
|
|
35
|
+
INITIAL_PEN = Pen::INITIAL
|
|
43
36
|
|
|
44
37
|
def self.render(bounds, body:, fill:, stroke:, stroke_width:, family:, size:, text_fill:, pen: nil)
|
|
45
38
|
new(bounds, body, fill, stroke, stroke_width, family, size, text_fill, pen || INITIAL_PEN).to_svg
|
|
@@ -33,13 +33,11 @@ module Ea
|
|
|
33
33
|
def groups
|
|
34
34
|
@drawn_bounds = {}
|
|
35
35
|
@divider_y = {}
|
|
36
|
-
@painted_any = false
|
|
37
36
|
@previous_pen = nil
|
|
38
37
|
ordered_elements.flat_map do |e|
|
|
39
|
-
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
groups
|
|
38
|
+
painted = groups_for(e)
|
|
39
|
+
@previous_pen = painted.last&.pen || @previous_pen
|
|
40
|
+
painted.map(&:svg)
|
|
43
41
|
end
|
|
44
42
|
end
|
|
45
43
|
|
|
@@ -58,9 +56,14 @@ module Ea
|
|
|
58
56
|
private
|
|
59
57
|
|
|
60
58
|
# EA paints placements by descending Sequence (the lowest
|
|
61
|
-
# Sequence is on top); equal keys keep their stored order.
|
|
59
|
+
# Sequence is on top); equal keys keep their stored order. A
|
|
60
|
+
# placement with no Sequence (no such row in the corpus, so EA's
|
|
61
|
+
# treatment is unmeasured) is painted first, as before the
|
|
62
|
+
# descending order, rather than ahead of the numbered ones.
|
|
62
63
|
def ordered_elements
|
|
63
|
-
(diagram.elements || []).sort_by.with_index
|
|
64
|
+
(diagram.elements || []).sort_by.with_index do |e, i|
|
|
65
|
+
e.z_order ? [1, -e.z_order, i] : [0, 0, i]
|
|
66
|
+
end
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def groups_for(element)
|
|
@@ -72,7 +75,7 @@ module Ea
|
|
|
72
75
|
record_drawn_bounds(element, context)
|
|
73
76
|
return [] if element.style&.fetch("connector_proxy", false)
|
|
74
77
|
|
|
75
|
-
Compartment.
|
|
78
|
+
Compartment.paint_all(context)
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
# Logical-space drawn bounds: stored x/y with the RENDERED
|
|
@@ -286,6 +289,7 @@ module Ea
|
|
|
286
289
|
height: grown_bottom + compartment_bottom_padding(size, family, size_unit) - bounds.y
|
|
287
290
|
)
|
|
288
291
|
end
|
|
292
|
+
bounds = grow_for_tagged_values(bounds, geometry, element, classifier)
|
|
289
293
|
RenderContext.new(
|
|
290
294
|
element: element,
|
|
291
295
|
bounds: bounds,
|
|
@@ -310,7 +314,6 @@ module Ea
|
|
|
310
314
|
diagram: diagram,
|
|
311
315
|
model_index: model_index,
|
|
312
316
|
off_canvas_parent_name: parent_name,
|
|
313
|
-
first_painted: !@painted_any,
|
|
314
317
|
previous_pen: @previous_pen
|
|
315
318
|
)
|
|
316
319
|
end
|
|
@@ -322,6 +325,21 @@ module Ea
|
|
|
322
325
|
offset
|
|
323
326
|
end
|
|
324
327
|
|
|
328
|
+
# A shown tags compartment that overflows the stored box grows
|
|
329
|
+
# it to the last tag row plus one row (204F4ACC AcmeUmlClass:
|
|
330
|
+
# QEA stores h70; EA draws h85, the XMI geometry already says 85).
|
|
331
|
+
# Fitted on that diagram only; boxes whose stored height already
|
|
332
|
+
# holds the tags are untouched.
|
|
333
|
+
def grow_for_tagged_values(bounds, geometry, element, classifier)
|
|
334
|
+
return bounds unless element.show_tagged_values && tagged_values_for(classifier).any?
|
|
335
|
+
|
|
336
|
+
tags_bottom = geometry.tagged_values_bottom_y
|
|
337
|
+
return bounds unless tags_bottom && tags_bottom > bounds.y + bounds.height
|
|
338
|
+
|
|
339
|
+
Ea::Model::Bounds.new(x: bounds.x, y: bounds.y, width: bounds.width,
|
|
340
|
+
height: tags_bottom - bounds.y)
|
|
341
|
+
end
|
|
342
|
+
|
|
325
343
|
def compartment_bottom_padding(size, family, size_unit)
|
|
326
344
|
Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit) ? 7 : 8
|
|
327
345
|
end
|
|
@@ -397,12 +415,18 @@ module Ea
|
|
|
397
415
|
# that were +1: 0114FD24 uro::AbstractConstruction etc.; 20 of
|
|
398
416
|
# 21 now exact, no other compared item moved). The tight ghost
|
|
399
417
|
# variant has no corpus case and keeps the default offsets.
|
|
418
|
+
#
|
|
419
|
+
# An Enumeration with literals takes it too (EMPIRICAL FIT, test
|
|
420
|
+
# B58D1A53 Enumeration: divider 346 = name 339 + 7; the only Cambria 7pt
|
|
421
|
+
# enumeration with literals in the small corpora).
|
|
400
422
|
def compact_header_offsets?(classifier, header_lines, size, **options)
|
|
401
|
-
|
|
423
|
+
literals = options[:enum_literals_count]
|
|
424
|
+
kind_fits = (classifier.is_a?(Ea::Model::Klass) && literals.zero?) ||
|
|
425
|
+
(classifier.is_a?(Ea::Model::Enumeration) && literals.positive?)
|
|
426
|
+
kind_fits &&
|
|
402
427
|
Ea::Fonts::Metrics.compact_compartment_font?(size, family: options[:family], unit: options[:size_unit]) &&
|
|
403
428
|
header_lines.dig(0, 1) != :italic_tight &&
|
|
404
|
-
options.values_at(:logical_height, :marker_count, :
|
|
405
|
-
:divider_offset, :attr_first_offset) == [nil, 0, 0, 8, 7] &&
|
|
429
|
+
options.values_at(:logical_height, :marker_count, :divider_offset, :attr_first_offset) == [nil, 0, 8, 7] &&
|
|
406
430
|
theme.compartments.header_line_offset == 6 && theme.compartments.attr_line_offset == 6
|
|
407
431
|
end
|
|
408
432
|
|
|
@@ -68,7 +68,9 @@ module Ea
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def logical_height_for(element)
|
|
71
|
-
|
|
71
|
+
return nil unless size_unit_for(element) == "pt"
|
|
72
|
+
|
|
73
|
+
Ea::Fonts::Metrics.source_logical_height(element&.font_logical_height)
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def weight_for(element)
|