ea 0.6.120 → 0.6.121
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/119.yml +2 -4
- data/lib/ea/fonts/carlito_metrics.json +1 -1
- data/lib/ea/fonts/metrics.rb +12 -2
- data/lib/ea/sources/qea/diagram_builder.rb +2 -2
- data/lib/ea/svg/ea_emitter/color_resolver.rb +1 -1
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +13 -12
- data/lib/ea/svg/ea_emitter/connector_redock.rb +3 -2
- data/lib/ea/svg/ea_emitter/connectors.rb +38 -9
- data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +4 -44
- data/lib/ea/svg/ea_emitter/elements.rb +7 -2
- data/lib/ea/svg/ea_emitter/font_resolver.rb +18 -3
- data/lib/ea/svg/ea_emitter/marker/arrow_path.rb +12 -1
- data/lib/ea/svg/ea_emitter/marker/diamond.rb +39 -3
- data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +1 -1
- data/lib/ea/svg/ea_emitter/marker/package_import.rb +6 -19
- data/lib/ea/svg/ea_emitter/marker/plus.rb +9 -17
- data/lib/ea/svg/ea_emitter/markers.rb +53 -64
- data/lib/ea/version.rb +1 -1
- metadata +1 -1
data/lib/ea/fonts/metrics.rb
CHANGED
|
@@ -51,6 +51,9 @@ module Ea
|
|
|
51
51
|
# Advance used for codepoints missing from the tables.
|
|
52
52
|
DEFAULT_ADVANCE = 0.25
|
|
53
53
|
|
|
54
|
+
# Inferred subpixel steps that reproduce EA's near-half-pixel rounding.
|
|
55
|
+
GDI_SUBPIXELS = 64
|
|
56
|
+
|
|
54
57
|
# Carlito/Arial have no CJK glyphs; EA's renderer falls back to
|
|
55
58
|
# a proportional CJK font whose advances measure ~0.709em at
|
|
56
59
|
# the NOMINAL size (not scaled by the per-style factors) —
|
|
@@ -89,6 +92,13 @@ module Ea
|
|
|
89
92
|
(size.to_f * 10 / 7).round
|
|
90
93
|
end
|
|
91
94
|
|
|
95
|
+
# Quantize before rounding: near-half-pixel Carlito advances
|
|
96
|
+
# round up in EA's output. The 1/64px step is inferred.
|
|
97
|
+
def gdi_pixel_advance(advance, height)
|
|
98
|
+
((advance * height * GDI_SUBPIXELS).round / GDI_SUBPIXELS.to_f).round
|
|
99
|
+
end
|
|
100
|
+
private_class_method :gdi_pixel_advance
|
|
101
|
+
|
|
92
102
|
def compact_compartment_font?(size, family:, unit: "pt")
|
|
93
103
|
family.to_s.casecmp("Cambria").zero? && size == 7 && unit == "pt"
|
|
94
104
|
end
|
|
@@ -122,7 +132,7 @@ module Ea
|
|
|
122
132
|
if cjk?(ch.ord)
|
|
123
133
|
cjk_count += 1
|
|
124
134
|
else
|
|
125
|
-
latin += (
|
|
135
|
+
latin += gdi_pixel_advance(table[cp_key(ch)] || DEFAULT_ADVANCE, height)
|
|
126
136
|
end
|
|
127
137
|
end
|
|
128
138
|
return latin + cjk_count * CJK_ADVANCE * size_pt.to_f
|
|
@@ -166,7 +176,7 @@ module Ea
|
|
|
166
176
|
|
|
167
177
|
# Families whose EA driver quantizes per-glyph advances
|
|
168
178
|
# (fitted from published textLengths).
|
|
169
|
-
GDI_HEIGHT_FAMILIES = %w[Carlito].freeze
|
|
179
|
+
GDI_HEIGHT_FAMILIES = %w[Carlito Arial].freeze
|
|
170
180
|
|
|
171
181
|
def supported_family?(family)
|
|
172
182
|
!resolve_family(family).nil?
|
|
@@ -150,14 +150,14 @@ module Ea
|
|
|
150
150
|
)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
# Negative widths inherit
|
|
153
|
+
# Negative widths inherit; QEA zero widths request a one-pixel cosmetic pen.
|
|
154
154
|
def effective_line_width(obj_row, style)
|
|
155
155
|
object = database.find_object(obj_row.ea_object_id)
|
|
156
156
|
[style[:lwth], object&.borderwidth].each do |raw|
|
|
157
157
|
next if raw.nil? || raw.to_s.empty?
|
|
158
158
|
|
|
159
159
|
width = raw.to_i
|
|
160
|
-
return width if width >= 0
|
|
160
|
+
return [width, 1].max if width >= 0
|
|
161
161
|
end
|
|
162
162
|
nil
|
|
163
163
|
end
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Single source of truth for fill + stroke color decisions.
|
|
7
7
|
#
|
|
8
8
|
# Theme :119 applies:
|
|
9
|
-
# -
|
|
9
|
+
# - Black default element strokes
|
|
10
10
|
# - Method color for operation text
|
|
11
11
|
# Theme :119 does NOT apply:
|
|
12
12
|
# - Per-type fill colors (elements use default #FFFFFF)
|
|
@@ -21,13 +21,8 @@ module Ea
|
|
|
21
21
|
%(<g style="#{group_style(context)}">\n#{text_blocks.join("\n")}\n</g>)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# EA wraps note text by MEASURED width (Carlito advances),
|
|
25
|
-
# not character count: lines break at the last word whose
|
|
26
|
-
# cumulative integer textLength fits bounds.width - 15.
|
|
27
|
-
# Verified against EA-published reference SVGs.
|
|
28
24
|
def wrapped_lines(context, body)
|
|
29
|
-
#
|
|
30
|
-
# (9BA1CE54: width 214, longest line tl 210). Pitch and
|
|
25
|
+
# Lines fit bounds.width - 4. Pitch and
|
|
31
26
|
# first-line offset scale with the note font (9pt: pitch
|
|
32
27
|
# 16 = size+7, first line at bounds.y + 18 = size+9), and
|
|
33
28
|
# each paragraph break adds one blank pitch row
|
|
@@ -41,7 +36,8 @@ module Ea
|
|
|
41
36
|
y_index = 0
|
|
42
37
|
paragraphs = NoteHtml.paragraphs(body)
|
|
43
38
|
paragraphs.flat_map do |para|
|
|
44
|
-
lines = wrap_paragraph(para.text, usable, size, context.family
|
|
39
|
+
lines = wrap_paragraph(para.text, usable, size, context.family,
|
|
40
|
+
logical_height: context.logical_height)
|
|
45
41
|
paragraph_svg = lines.each_with_index.map do |line, i|
|
|
46
42
|
y = context.bounds.y + first_offset + ((y_index + i) * pitch)
|
|
47
43
|
TextRenderer.new(
|
|
@@ -50,6 +46,7 @@ module Ea
|
|
|
50
46
|
y: y,
|
|
51
47
|
family: context.family, size: context.size,
|
|
52
48
|
size_unit: context.size_unit,
|
|
49
|
+
logical_height: context.logical_height,
|
|
53
50
|
fill: context.theme.text_color,
|
|
54
51
|
stroke_in_text: context.theme.stroke_in_text_color,
|
|
55
52
|
width_factor: context.theme.text_width_factor
|
|
@@ -60,14 +57,18 @@ module Ea
|
|
|
60
57
|
end
|
|
61
58
|
end
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
# Wrap at whitespace until native script-specific break metrics are
|
|
61
|
+
# established. Every word but the paragraph's last carries its
|
|
62
|
+
# trailing space into the fit test.
|
|
63
|
+
def wrap_paragraph(para, usable, size, family, logical_height: nil)
|
|
65
64
|
lines = []
|
|
66
65
|
current = ""
|
|
67
|
-
words
|
|
66
|
+
words = para.strip.split(/\s+/)
|
|
67
|
+
words.each_with_index do |word, index|
|
|
68
68
|
candidate = current.empty? ? word : "#{current} #{word}"
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
trailing = index < words.size - 1 ? " " : ""
|
|
70
|
+
width = TextRenderer.estimate_width("#{candidate}#{trailing}", size, nil,
|
|
71
|
+
family: family, logical_height: logical_height).round
|
|
71
72
|
if current.empty? || width <= usable
|
|
72
73
|
current = candidate
|
|
73
74
|
else
|
|
@@ -233,7 +233,8 @@ module Ea
|
|
|
233
233
|
private_class_method :facing_coordinate
|
|
234
234
|
|
|
235
235
|
def stroke_outline?(connector, bounds)
|
|
236
|
-
(connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector)
|
|
236
|
+
(connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector) ||
|
|
237
|
+
connector.connector_type == "NoteLink") &&
|
|
237
238
|
bounds.all? { |box| box.width.positive? && box.height.positive? } &&
|
|
238
239
|
outline_connector?(connector, bounds)
|
|
239
240
|
end
|
|
@@ -311,7 +312,7 @@ module Ea
|
|
|
311
312
|
|
|
312
313
|
def outline_connector?(connector, bounds)
|
|
313
314
|
return true if default_model_connector?(connector)
|
|
314
|
-
return true if %w[Generalization Aggregation].include?(connector.connector_type)
|
|
315
|
+
return true if %w[Generalization Aggregation NoteLink].include?(connector.connector_type)
|
|
315
316
|
|
|
316
317
|
return true if association_ray_outline?(connector)
|
|
317
318
|
return false unless stored_association?(connector) || offset_free_association?(connector)
|
|
@@ -31,22 +31,39 @@ module Ea
|
|
|
31
31
|
@divider_y_by_ref = divider_y_by_ref
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# EA draws each connector (and its markers) with the pen width
|
|
35
|
+
# stored in the diagram link style (`LWidth`); links without
|
|
36
|
+
# the field keep the diagram default.
|
|
37
|
+
def self.stroke_width_for(connector, default)
|
|
38
|
+
style = connector.style || {}
|
|
39
|
+
raw = style[:lwidth] || style["lwidth"]
|
|
40
|
+
return default if raw.to_s.strip.empty?
|
|
41
|
+
|
|
42
|
+
[raw.to_i, 1].max
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Layer key for a style group: the plain key at the default
|
|
46
|
+
# width, suffixed with the width otherwise so layers of
|
|
47
|
+
# different widths never merge.
|
|
48
|
+
def self.layer_key(key, width, default)
|
|
49
|
+
width == default ? key : :"#{key}_w#{width}"
|
|
50
|
+
end
|
|
51
|
+
|
|
34
52
|
def layers
|
|
35
|
-
|
|
36
|
-
return [] if paths.empty?
|
|
53
|
+
widths = paths_with_widths
|
|
37
54
|
|
|
38
55
|
if grouped
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else
|
|
42
|
-
paths.map do |path|
|
|
43
|
-
Layer.new(style_key: LINE_STYLE_KEY, style: line_style, body: path)
|
|
56
|
+
widths.group_by(&:first).map do |width, entries|
|
|
57
|
+
line_layer(width, entries.map(&:last).join("\n "))
|
|
44
58
|
end
|
|
59
|
+
else
|
|
60
|
+
widths.map { |width, path| line_layer(width, path) }
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
63
|
|
|
48
|
-
def line_style
|
|
49
|
-
"stroke-width:#{
|
|
64
|
+
def line_style(width = stroke_width)
|
|
65
|
+
"stroke-width:#{width};stroke-linecap:round;stroke-linejoin:bevel; " \
|
|
66
|
+
"fill:#000000;fill-opacity:0.00; stroke:#000000; stroke-opacity:1.00"
|
|
50
67
|
end
|
|
51
68
|
|
|
52
69
|
# Backwards-compatible render — joins all layers' `<g>`.
|
|
@@ -56,6 +73,18 @@ module Ea
|
|
|
56
73
|
|
|
57
74
|
private
|
|
58
75
|
|
|
76
|
+
def paths_with_widths
|
|
77
|
+
visible_connectors.filter_map do |connector|
|
|
78
|
+
path = path_for(connector)
|
|
79
|
+
[self.class.stroke_width_for(connector, stroke_width), path] if path
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def line_layer(width, body)
|
|
84
|
+
Layer.new(style_key: self.class.layer_key(LINE_STYLE_KEY, width, stroke_width),
|
|
85
|
+
style: line_style(width), body: body)
|
|
86
|
+
end
|
|
87
|
+
|
|
59
88
|
def visible_connectors
|
|
60
89
|
(diagram.connectors || []).select(&:renderable?)
|
|
61
90
|
end
|
|
@@ -9,32 +9,15 @@ module Ea
|
|
|
9
9
|
# chevron) when a stereotype provides a custom icon via
|
|
10
10
|
# ShapeScript.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
# common GML stereotypes (FeatureType, Type) since the source
|
|
14
|
-
# ShapeScript definitions live in EA's encrypted
|
|
15
|
-
# InternalTechnologies (TODO.diagrams/87). Future enhancement:
|
|
16
|
-
# read shapescript from MDG files when available.
|
|
12
|
+
# Only MDG-provided ShapeScript is rendered.
|
|
17
13
|
class StereotypeIconRenderer
|
|
18
|
-
# Hardcoded fallback icons keyed by stereotype name.
|
|
19
|
-
# Each entry is [fill, stroke, points_array].
|
|
20
|
-
FALLBACK_ICONS = {
|
|
21
|
-
"FeatureType" => ["#FAF1EC", "#69738C",
|
|
22
|
-
[[0, -5], [5, 0], [0, 5], [-5, 0]]],
|
|
23
|
-
"Type" => ["#FAF1EC", "#69738C",
|
|
24
|
-
[[0, -5], [5, 0], [0, 5], [-5, 0]]]
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
# Default position offset from element's center
|
|
28
|
-
OFFSET_X = 0
|
|
29
|
-
OFFSET_Y = 0
|
|
30
|
-
|
|
31
14
|
attr_reader :classifier, :bounds, :canvas, :mdg_registry
|
|
32
15
|
|
|
33
16
|
# @param mdg_registry [Ea::Mdg::Registry, nil] optional
|
|
34
17
|
# registry to look up MDG-provided ShapeScript for the
|
|
35
18
|
# classifier's stereotype. When provided and the matching
|
|
36
19
|
# stereotype has a ShapeScript body, it's parsed and
|
|
37
|
-
# rendered.
|
|
20
|
+
# rendered.
|
|
38
21
|
def initialize(classifier:, bounds:, canvas: nil, mdg_registry: nil)
|
|
39
22
|
@classifier = classifier
|
|
40
23
|
@bounds = bounds
|
|
@@ -51,12 +34,12 @@ module Ea
|
|
|
51
34
|
return "" unless classifier && bounds
|
|
52
35
|
return "" unless stereotype_name
|
|
53
36
|
|
|
54
|
-
shapescript_svg
|
|
37
|
+
shapescript_svg.to_s
|
|
55
38
|
end
|
|
56
39
|
|
|
57
40
|
private
|
|
58
41
|
|
|
59
|
-
#
|
|
42
|
+
# Renders MDG-provided ShapeScript for the stereotype.
|
|
60
43
|
# @return [String, nil] SVG fragment from parsed ShapeScript
|
|
61
44
|
def shapescript_svg
|
|
62
45
|
return nil unless mdg_registry
|
|
@@ -83,35 +66,12 @@ module Ea
|
|
|
83
66
|
nil
|
|
84
67
|
end
|
|
85
68
|
|
|
86
|
-
# @return [String] hardcoded fallback polygon, or "" if none
|
|
87
|
-
def fallback_svg
|
|
88
|
-
spec = FALLBACK_ICONS[stereotype_name]
|
|
89
|
-
return "" unless spec
|
|
90
|
-
|
|
91
|
-
fill, stroke, points = spec
|
|
92
|
-
cx = bounds.x + bounds.width / 2 + OFFSET_X
|
|
93
|
-
cy = bounds.y + bounds.height / 2 + OFFSET_Y
|
|
94
|
-
translated = points.map { |x, y| translate_point(cx + x, cy + y) }
|
|
95
|
-
pts_str = translated.map { |x, y| coord(x) + "," + coord(y) }.join(" ")
|
|
96
|
-
%(<polygon points="#{pts_str}" fill="#{fill}" stroke="#{stroke}" stroke-width="1"/>)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
69
|
def stereotype_name
|
|
100
70
|
return nil unless classifier.is_a?(Ea::Model::Classifier)
|
|
101
71
|
|
|
102
72
|
refs = classifier.stereotype_refs
|
|
103
73
|
refs&.first
|
|
104
74
|
end
|
|
105
|
-
|
|
106
|
-
def translate_point(x, y)
|
|
107
|
-
return [x, y] unless canvas
|
|
108
|
-
|
|
109
|
-
[canvas.translate_x(x), canvas.translate_y(y)]
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def coord(value)
|
|
113
|
-
Ea::Svg::EaEmitter::Canvas.coord(value)
|
|
114
|
-
end
|
|
115
75
|
end
|
|
116
76
|
end
|
|
117
77
|
end
|
|
@@ -11,7 +11,7 @@ module Ea
|
|
|
11
11
|
class Elements
|
|
12
12
|
DEFAULT_FILL = "#FFFFFF"
|
|
13
13
|
DEFAULT_STROKE = "#000000"
|
|
14
|
-
DEFAULT_STROKE_WIDTH =
|
|
14
|
+
DEFAULT_STROKE_WIDTH = 1
|
|
15
15
|
DEFAULT_TEXT_COLOR = "#000000"
|
|
16
16
|
|
|
17
17
|
attr_reader :diagram, :model_index, :canvas, :document
|
|
@@ -178,6 +178,9 @@ module Ea
|
|
|
178
178
|
off_canvas_parent_name: parent_name,
|
|
179
179
|
foreign_package_name: foreign_package_name_for(classifier),
|
|
180
180
|
suppress_stereotypes: suppress_stereotypes?) : []
|
|
181
|
+
header_lines = header_lines.map do |text, style|
|
|
182
|
+
[text, font_resolver.header_style_for(style, element)]
|
|
183
|
+
end
|
|
181
184
|
geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
182
185
|
tagged_values_for(classifier).size,
|
|
183
186
|
header_lines, classifier: classifier, family: family,
|
|
@@ -257,9 +260,11 @@ module Ea
|
|
|
257
260
|
# Bag': Set enum box stored h59 -> EA 223 fitting six
|
|
258
261
|
# literals); op-only boxes keep the no-growth behavior
|
|
259
262
|
# (op_bottom_y is a divider sentinel, not content).
|
|
263
|
+
# Saved boxes may already fit rows with less than the growth
|
|
264
|
+
# padding. Test the baseline for overflow before adding padding.
|
|
260
265
|
if (attr_lines.any? || enum_literals_for(classifier).any?) &&
|
|
261
266
|
grown_bottom.positive? &&
|
|
262
|
-
grown_bottom
|
|
267
|
+
grown_bottom > bounds.y + bounds.height
|
|
263
268
|
bounds = Ea::Model::Bounds.new(
|
|
264
269
|
x: bounds.x, y: bounds.y,
|
|
265
270
|
width: bounds.width,
|
|
@@ -43,7 +43,7 @@ module Ea
|
|
|
43
43
|
return explicit
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
default_family_for(explicit)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def size_for(element)
|
|
@@ -72,14 +72,29 @@ module Ea
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def style_for(element)
|
|
75
|
-
|
|
75
|
+
return "italic" if element&.font_italic
|
|
76
|
+
return "normal" if element&.style&.key?(:italic)
|
|
77
|
+
|
|
78
|
+
diagram.hand_draw ? "italic" : "normal"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def header_style_for(style, element)
|
|
82
|
+
return style unless diagram.hand_draw && style_for(element) == "italic"
|
|
83
|
+
|
|
84
|
+
style == :bold ? :bold_italic : style
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
private
|
|
79
88
|
|
|
89
|
+
def default_family_for(explicit)
|
|
90
|
+
return "Source Sans Pro" if diagram.hand_draw
|
|
91
|
+
|
|
92
|
+
theme_family_for(explicit) || diagram_default_family || DEFAULT_FONT_FAMILY
|
|
93
|
+
end
|
|
94
|
+
|
|
80
95
|
def theme_family_for(explicit)
|
|
81
96
|
return unless theme.themed?
|
|
82
|
-
return theme.font_family if DEFAULT_MARKER_FAMILIES.include?(explicit)
|
|
97
|
+
return theme.font_family || "Carlito" if DEFAULT_MARKER_FAMILIES.include?(explicit)
|
|
83
98
|
|
|
84
99
|
theme.element_font_family || theme.font_family
|
|
85
100
|
end
|
|
@@ -4,7 +4,9 @@ module Ea
|
|
|
4
4
|
module Svg
|
|
5
5
|
module EaEmitter
|
|
6
6
|
module Marker
|
|
7
|
-
# Navigability arrow at the target end of an association
|
|
7
|
+
# Navigability arrow at the target end of an association, plus
|
|
8
|
+
# the diamond of an association whose end carries an aggregate
|
|
9
|
+
# flag (SourceIsAggregate/DestIsAggregate).
|
|
8
10
|
# Rendered as a `<path>` (not polygon) sharing style with the
|
|
9
11
|
# connector line. When both ends are navigable
|
|
10
12
|
# (bidirectional association), EA renders a separate arrow at
|
|
@@ -16,6 +18,14 @@ module Ea
|
|
|
16
18
|
|
|
17
19
|
def self.specs_for(connector, source, target, before_target,
|
|
18
20
|
after_source, relationship: nil)
|
|
21
|
+
Diamond.flagged_end_specs(relationship, source, target,
|
|
22
|
+
before_target, after_source) +
|
|
23
|
+
arrow_specs(connector, source, target, before_target,
|
|
24
|
+
after_source)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.arrow_specs(connector, source, target, before_target,
|
|
28
|
+
after_source)
|
|
19
29
|
# EA draws the navigability arrow on directed
|
|
20
30
|
# associations AND aggregations (333EFCCF: 6 aggregations
|
|
21
31
|
# render diamond + path arrow); Direction
|
|
@@ -33,6 +43,7 @@ module Ea
|
|
|
33
43
|
base = whole_end_at_source ? before_target : after_source
|
|
34
44
|
[Registry::Spec.new(shape: :arrow, anchor: anchor, base: base)]
|
|
35
45
|
end
|
|
46
|
+
private_class_method :arrow_specs
|
|
36
47
|
|
|
37
48
|
# EA marks truly bidirectional associations with
|
|
38
49
|
# direction="Bi-Directional" on t_connector. These get a
|
|
@@ -4,8 +4,9 @@ module Ea
|
|
|
4
4
|
module Svg
|
|
5
5
|
module EaEmitter
|
|
6
6
|
module Marker
|
|
7
|
-
# EA endpoint fields select the diamond end
|
|
8
|
-
# the arrow end
|
|
7
|
+
# EA endpoint fields select the diamond end and its fill (shared
|
|
8
|
+
# is hollow, composite is solid); direction selects the arrow end
|
|
9
|
+
# for target-owned aggregations.
|
|
9
10
|
class Diamond < Kind
|
|
10
11
|
def self.handles?(effective_type)
|
|
11
12
|
%w[Aggregation Composition].include?(effective_type)
|
|
@@ -15,7 +16,7 @@ module Ea
|
|
|
15
16
|
whole_at_target = whole_at_target?(connector, relationship)
|
|
16
17
|
whole_anchor = whole_at_target ? target : source
|
|
17
18
|
whole_base = whole_at_target ? before_target : after_source
|
|
18
|
-
specs = [Registry::Spec.new(shape:
|
|
19
|
+
specs = [Registry::Spec.new(shape: diamond_shape(connector, relationship, whole_at_target),
|
|
19
20
|
anchor: whole_anchor,
|
|
20
21
|
base: whole_base)]
|
|
21
22
|
return specs unless whole_at_target
|
|
@@ -24,6 +25,41 @@ module Ea
|
|
|
24
25
|
before_target, after_source))
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# Diamonds for the ends of a plain association that carry an
|
|
29
|
+
# aggregate flag.
|
|
30
|
+
def self.flagged_end_specs(relationship, source, target,
|
|
31
|
+
before_target, after_source)
|
|
32
|
+
return [] unless relationship
|
|
33
|
+
|
|
34
|
+
{ target_aggregation: [target, before_target],
|
|
35
|
+
source_aggregation: [source, after_source] }.filter_map do |reader, (anchor, base)|
|
|
36
|
+
shape = flagged_shape(relationship.public_send(reader))
|
|
37
|
+
Registry::Spec.new(shape: shape, anchor: anchor, base: base) if shape
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.flagged_shape(kind)
|
|
42
|
+
case kind.to_s
|
|
43
|
+
when "shared" then :diamond_hollow
|
|
44
|
+
when "composite" then :diamond
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
private_class_method :flagged_shape
|
|
48
|
+
|
|
49
|
+
def self.diamond_shape(connector, relationship, whole_at_target)
|
|
50
|
+
kind = if relationship
|
|
51
|
+
whole_at_target ? relationship.target_aggregation : relationship.source_aggregation
|
|
52
|
+
end
|
|
53
|
+
kind = type_default_kind(connector) if kind.to_s.empty? || kind.to_s == "none"
|
|
54
|
+
kind.to_s == "shared" ? :diamond_hollow : :diamond
|
|
55
|
+
end
|
|
56
|
+
private_class_method :diamond_shape
|
|
57
|
+
|
|
58
|
+
def self.type_default_kind(connector)
|
|
59
|
+
connector.connector_type == "Composition" ? "composite" : "shared"
|
|
60
|
+
end
|
|
61
|
+
private_class_method :type_default_kind
|
|
62
|
+
|
|
27
63
|
def self.whole_at_target?(connector, relationship)
|
|
28
64
|
return true if dest_flagged?(relationship)
|
|
29
65
|
return false if source_flagged?(relationship)
|
|
@@ -15,7 +15,7 @@ module Ea
|
|
|
15
15
|
class OpenTriangle < Kind
|
|
16
16
|
def self.handles?(effective_type)
|
|
17
17
|
%w[Generalization Realization Realisation Dependency
|
|
18
|
-
InformationFlow
|
|
18
|
+
InformationFlow].include?(effective_type)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
|
|
@@ -5,30 +5,17 @@ module Ea
|
|
|
5
5
|
module EaEmitter
|
|
6
6
|
module Marker
|
|
7
7
|
# PackageImport marker for «import» connectors between
|
|
8
|
-
# packages. EA draws
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# connector's geometry string carries explicit SX/SY offset
|
|
12
|
-
# fields. Auto-routed connectors (no SX/SY) render as a
|
|
13
|
-
# plain solid line with no package_anchor marker.
|
|
14
|
-
#
|
|
15
|
-
# Verified against both QEAs (output by the same EA run):
|
|
16
|
-
# - basic.qea Package Imports: SX=0;SY=0 present → marker
|
|
17
|
-
# - plateau Urban Planning ADE2: SX/SY absent → no marker
|
|
8
|
+
# packages. EA draws the dependency-style open arrowhead at the
|
|
9
|
+
# target (imported) end, whether or not the diagram link
|
|
10
|
+
# carries explicit SX/SY offsets.
|
|
18
11
|
class PackageImport < Kind
|
|
19
12
|
def self.handles?(effective_type)
|
|
20
13
|
effective_type == "Package"
|
|
21
14
|
end
|
|
22
15
|
|
|
23
|
-
def self.specs_for(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
Registry::Spec.new(shape: :package_anchor,
|
|
28
|
-
anchor: source, base: after_source),
|
|
29
|
-
Registry::Spec.new(shape: :arrow, anchor: target,
|
|
30
|
-
base: before_target)
|
|
31
|
-
]
|
|
16
|
+
def self.specs_for(_connector, _source, target, before_target, _after_source, **)
|
|
17
|
+
[Registry::Spec.new(shape: :dependency_arrow,
|
|
18
|
+
anchor: target, base: before_target)]
|
|
32
19
|
end
|
|
33
20
|
end
|
|
34
21
|
end
|
|
@@ -4,16 +4,12 @@ module Ea
|
|
|
4
4
|
module Svg
|
|
5
5
|
module EaEmitter
|
|
6
6
|
module Marker
|
|
7
|
-
#
|
|
8
|
-
# connector. EA
|
|
9
|
-
#
|
|
7
|
+
# Circled-plus marker at the container end of a Nesting
|
|
8
|
+
# connector. EA draws a white circle of radius ARM_LENGTH just
|
|
9
|
+
# outside the container edge with a plus (two crossed arms)
|
|
10
|
+
# through its centre; the connector starts at the nested
|
|
11
|
+
# element and ends at the container.
|
|
10
12
|
#
|
|
11
|
-
# Path encoding (two sub-paths in one <path>):
|
|
12
|
-
#
|
|
13
|
-
# M cx cy-8 L cx cy+8 (vertical arm, 16 tall)
|
|
14
|
-
# M cx+8 cy L cx-8 cy (horizontal arm, 16 wide)
|
|
15
|
-
#
|
|
16
|
-
# cx, cy = plus center, anchored at the source-end waypoint.
|
|
17
13
|
class Plus < Kind
|
|
18
14
|
ARM_LENGTH = 8
|
|
19
15
|
|
|
@@ -22,14 +18,10 @@ module Ea
|
|
|
22
18
|
end
|
|
23
19
|
|
|
24
20
|
def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
whole_end_at_source = whole_end_at_source?(connector)
|
|
30
|
-
anchor = whole_end_at_source ? source : target
|
|
31
|
-
base = whole_end_at_source ? after_source : before_target
|
|
32
|
-
[Registry::Spec.new(shape: :plus, anchor: anchor, base: base)]
|
|
21
|
+
container_at_target = whole_end_at_source?(connector)
|
|
22
|
+
anchor = container_at_target ? target : source
|
|
23
|
+
base = container_at_target ? before_target : after_source
|
|
24
|
+
[Registry::Spec.new(shape: :nesting, anchor: anchor, base: base)]
|
|
33
25
|
end
|
|
34
26
|
end
|
|
35
27
|
end
|