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
|
@@ -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
|
|
@@ -310,7 +313,6 @@ module Ea
|
|
|
310
313
|
diagram: diagram,
|
|
311
314
|
model_index: model_index,
|
|
312
315
|
off_canvas_parent_name: parent_name,
|
|
313
|
-
first_painted: !@painted_any,
|
|
314
316
|
previous_pen: @previous_pen
|
|
315
317
|
)
|
|
316
318
|
end
|
|
@@ -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)
|
|
@@ -22,6 +22,9 @@ module Ea
|
|
|
22
22
|
SEAT_FONT_SIZE = 7
|
|
23
23
|
SEAT_X_INSET = 3
|
|
24
24
|
SEAT_STEREOTYPE_GAP = 12
|
|
25
|
+
SEAT_STEREOTYPE_ONLY_INSET = 8
|
|
26
|
+
SEAT_STEREOTYPE_ONLY_DROP = 15
|
|
27
|
+
SEAT_MULT_DROP = 15
|
|
25
28
|
# EA keeps an end-label role's left edge at diagram x >= 3; the
|
|
26
29
|
# stereotype line moves with the role. Measured on the Plateau
|
|
27
30
|
# v5.1 export: of 776 saved end labels, the only three whose
|
|
@@ -142,6 +145,7 @@ module Ea
|
|
|
142
145
|
mult_pos = target_multiplicity_seat(connector, heading, anchor, mult_box) ||
|
|
143
146
|
target_diagonal_property_multiplicity(connector, heading, anchor, text_box, mult_box) ||
|
|
144
147
|
face_multiplicity || target_horizontal_multiplicity(heading, anchor, mult_box, mult) ||
|
|
148
|
+
target_sloped_rightward_multiplicity(connector, heading, anchor, mult_box, role, mult) ||
|
|
145
149
|
mult_pos
|
|
146
150
|
end
|
|
147
151
|
|
|
@@ -152,10 +156,14 @@ module Ea
|
|
|
152
156
|
[text_box, mult_box], mult) || mult_pos
|
|
153
157
|
if external_upward_property_source?(connector, heading, anchor, text_box, source_bounds)
|
|
154
158
|
text_pos = external_upward_property_role(anchor, role) || text_pos
|
|
159
|
+
mult_pos = external_upward_property_multiplicity(anchor, mult) || mult_pos
|
|
155
160
|
end
|
|
156
161
|
end
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
if phantom_property_box?(connector, direction, text_box) ||
|
|
164
|
+
rightward_phantom_property_box?(connector, direction, text_box)
|
|
165
|
+
text_box = text_box.merge("alignment" => 1)
|
|
166
|
+
end
|
|
159
167
|
|
|
160
168
|
if end_kind == :target && !from_property
|
|
161
169
|
bend_positions = target_bend_positions(connector, bend, text_box, mult_box, role)
|
|
@@ -325,12 +333,30 @@ module Ea
|
|
|
325
333
|
# fallback.
|
|
326
334
|
def own_stereotype_only(text_box, anchor, connector, end_kind)
|
|
327
335
|
stereo = connector_source_dest_stereotype(connector, end_kind)
|
|
328
|
-
pos =
|
|
336
|
+
pos = stereotype_only_seat(text_box, anchor, connector, end_kind, stereo) ||
|
|
337
|
+
position_at(text_box, anchor)
|
|
329
338
|
return [] if stereo.nil? || stereo.empty? || pos.nil?
|
|
330
339
|
|
|
331
340
|
[text_at(pos[0], pos[1], "«#{stereo}»")]
|
|
332
341
|
end
|
|
333
342
|
|
|
343
|
+
# EMPIRICAL FIT (34FF671B and 5038EEE3, the only two ends in the
|
|
344
|
+
# Plateau v5.1 corpus that draw a Property stereotype with no role
|
|
345
|
+
# or multiplicity): a one-line centred target box is seated at its
|
|
346
|
+
# corner, anchor + (OX, OY) with no sign flip, like the source-end
|
|
347
|
+
# corner seat; the stereotype ends 8px left of the corner and its
|
|
348
|
+
# baseline sits 15px below it. Both reproduce the native SVG exactly.
|
|
349
|
+
def stereotype_only_seat(text_box, anchor, connector, end_kind, stereo)
|
|
350
|
+
return nil unless end_kind == :target && property_label?(connector, end_kind)
|
|
351
|
+
return nil unless seat_font? && cambria_box?(text_box) && text_box.values_at("alignment", "cy") == [1, 16]
|
|
352
|
+
|
|
353
|
+
width = Ea::Fonts::Metrics.text_length("«#{stereo}»", font_size, family: font_family)
|
|
354
|
+
return nil unless width
|
|
355
|
+
|
|
356
|
+
x, y = seat_corner(anchor, text_box)
|
|
357
|
+
[x - width - SEAT_STEREOTYPE_ONLY_INSET, y + SEAT_STEREOTYPE_ONLY_DROP]
|
|
358
|
+
end
|
|
359
|
+
|
|
334
360
|
# EA's tree-route label clusters (hand-verified against the
|
|
335
361
|
# EA-published reference, SY 8542 with canvas=(0,0)):
|
|
336
362
|
# source-end pairs stack at the exit point (role +3/-23,
|
|
@@ -374,8 +400,8 @@ module Ea
|
|
|
374
400
|
role.to_s.length * 6
|
|
375
401
|
mult_width = Ea::Fonts::Metrics.text_length(mult.to_s, font_size, family: font_family) ||
|
|
376
402
|
mult.to_s.length * 6
|
|
377
|
-
[[anchor[0] - 60 - role_width, anchor[1] - 7],
|
|
378
|
-
[anchor[0] - mult_width - 3, anchor[1] + 15]]
|
|
403
|
+
[[[anchor[0] - 60 - role_width, MIN_ROLE_X].max, anchor[1] - 7],
|
|
404
|
+
[[anchor[0] - mult_width - 3, MIN_ROLE_X].max, anchor[1] + 15]]
|
|
379
405
|
end
|
|
380
406
|
|
|
381
407
|
# Stored source-end boxes on a 7pt label whose first leg
|
|
@@ -461,10 +487,10 @@ module Ea
|
|
|
461
487
|
end
|
|
462
488
|
|
|
463
489
|
# Measured box heights: left/down Property clusters store CY 35,
|
|
464
|
-
# right-seat clusters CY 31. Phantom outlines leaving down
|
|
465
|
-
# synthesized zero box that centers the same way.
|
|
490
|
+
# right-seat and up clusters CY 31. Phantom outlines leaving down
|
|
491
|
+
# carry a synthesized zero box that centers the same way.
|
|
466
492
|
def centered_property_box?(connector, direction, box)
|
|
467
|
-
cy = { left: 35, down: 35, right: 31 }[direction]
|
|
493
|
+
cy = { left: 35, down: 35, right: 31, up: 31 }[direction]
|
|
468
494
|
(cy && box.values_at("cy", "alignment") == [cy, 1]) ||
|
|
469
495
|
(direction == :down && phantom_property_box?(connector, direction, box))
|
|
470
496
|
end
|
|
@@ -486,6 +512,16 @@ module Ea
|
|
|
486
512
|
property_label?(connector, :source)
|
|
487
513
|
end
|
|
488
514
|
|
|
515
|
+
# A rightward phantom keeps the plain role seat; only its stereotype
|
|
516
|
+
# is centred under the role (0BE81AC5: native stereotype 4px right of
|
|
517
|
+
# the role for a 52px role over a 44px stereotype).
|
|
518
|
+
def rightward_phantom_property_box?(connector, direction, box)
|
|
519
|
+
direction == :right && seat_font? && cambria_box?(box) &&
|
|
520
|
+
box["alignment"].nil? && stored_offsets_zero?(box) &&
|
|
521
|
+
connector.style.to_h[:phantom_outline] == true &&
|
|
522
|
+
property_label?(connector, :source)
|
|
523
|
+
end
|
|
524
|
+
|
|
489
525
|
def property_role_x(direction, origin, offset)
|
|
490
526
|
direction == :right ? origin + offset : origin - offset
|
|
491
527
|
end
|
|
@@ -526,6 +562,17 @@ module Ea
|
|
|
526
562
|
[[anchor[0] - inset - width, MIN_ROLE_X].max, anchor[1] + baseline]
|
|
527
563
|
end
|
|
528
564
|
|
|
565
|
+
# Multiplicity of a floating upward source (5038EEE3: both such
|
|
566
|
+
# ends, QEA; native "0..1" at anchor.x - width - 3, anchor.y + 15).
|
|
567
|
+
def external_upward_property_multiplicity(anchor, mult)
|
|
568
|
+
return nil if mult_empty?(mult)
|
|
569
|
+
|
|
570
|
+
width = Ea::Fonts::Metrics.text_length(mult, font_size, family: font_family)
|
|
571
|
+
return nil unless width
|
|
572
|
+
|
|
573
|
+
[anchor[0] - width - SEAT_X_INSET, anchor[1] + SEAT_MULT_DROP]
|
|
574
|
+
end
|
|
575
|
+
|
|
529
576
|
def external_upward_property_source?(connector, heading, anchor, box, bounds)
|
|
530
577
|
upward_source_heading?(heading) &&
|
|
531
578
|
zero_source_property_box?(box) && cambria_box?(box) && source_property_cambria?(connector) &&
|
|
@@ -886,6 +933,18 @@ module Ea
|
|
|
886
933
|
mult_seat(:right, anchor, box)
|
|
887
934
|
end
|
|
888
935
|
|
|
936
|
+
# EMPIRICAL FIT (5038EEE3, the only connector in the Plateau v5.1
|
|
937
|
+
# corpus with a Property source and a role-less target whose leg
|
|
938
|
+
# arrives sloped from the right):
|
|
939
|
+
# the multiplicity takes the rightward corner seat, as on a
|
|
940
|
+
# horizontal rightward leg. Reproduces the native SVG exactly.
|
|
941
|
+
def target_sloped_rightward_multiplicity(connector, heading, anchor, box, role, mult)
|
|
942
|
+
return nil unless seat_font? && role_empty?(role) && !mult_empty?(mult)
|
|
943
|
+
return nil unless heading && heading[0] > heading[1].abs && property_label?(connector, :source)
|
|
944
|
+
|
|
945
|
+
mult_seat(:right, anchor, box)
|
|
946
|
+
end
|
|
947
|
+
|
|
889
948
|
def target_left_face_multiplicity?(connector, heading, box, mult, target_face)
|
|
890
949
|
seat_font? && !mult_empty?(mult) && property_label?(connector, :target) && cambria_box?(box) &&
|
|
891
950
|
stored_offsets_zero?(box) && target_left_face_heading?(heading, target_face)
|
|
@@ -905,9 +964,12 @@ module Ea
|
|
|
905
964
|
end
|
|
906
965
|
|
|
907
966
|
def target_multiplicity_box?(connector, box)
|
|
908
|
-
# Property targets
|
|
909
|
-
# retain their explicit
|
|
910
|
-
|
|
967
|
+
# Property targets and vertical tree targets (5038EEE3) seat at
|
|
968
|
+
# the stored corner; plain moved labels retain their explicit
|
|
969
|
+
# offsets.
|
|
970
|
+
cambria_box?(box) &&
|
|
971
|
+
(stored_offsets_zero?(box) || property_label?(connector, :target) ||
|
|
972
|
+
connector.style.to_h[:tree] == "LV")
|
|
911
973
|
end
|
|
912
974
|
|
|
913
975
|
def cambria_box?(box)
|
|
@@ -91,8 +91,8 @@ module Ea
|
|
|
91
91
|
stored_tgt = end_boxes(boxes, :lrt, :lrb)
|
|
92
92
|
umldi_bounds = connector.umldi_label_bounds || {}
|
|
93
93
|
stored_labels = !(stored_src.nil? && stored_tgt.nil?) || !umldi_bounds.empty?
|
|
94
|
-
src_tb, src_mb = stored_src || unstored_label_boxes(points, :source, stored_labels)
|
|
95
|
-
tgt_tb, tgt_mb = stored_tgt || unstored_label_boxes(points, :target, stored_labels)
|
|
94
|
+
src_tb, src_mb = stored_src || unstored_label_boxes(points, :source, stored_labels, connector)
|
|
95
|
+
tgt_tb, tgt_mb = stored_tgt || unstored_label_boxes(points, :target, stored_labels, connector)
|
|
96
96
|
|
|
97
97
|
texts = []
|
|
98
98
|
end_renderer.texts(text_box: src_tb, mult_box: src_mb,
|
|
@@ -126,8 +126,15 @@ module Ea
|
|
|
126
126
|
|
|
127
127
|
# A connector that stores label boxes for only one end seats the
|
|
128
128
|
# other end's labels at its anchor, like a stored zero offset.
|
|
129
|
-
|
|
129
|
+
#
|
|
130
|
+
# A phantom outline has no saved link row, so EA draws its default
|
|
131
|
+
# (centred) label: the synthesized zero box carries no alignment,
|
|
132
|
+
# exactly like the QEA phantom's DEFAULT_LABEL_BOXES. Measured on
|
|
133
|
+
# the Plateau v5.1 export (A081C63E: 4 «property» lines, centred
|
|
134
|
+
# under the role in the native SVG, left-aligned from XMI).
|
|
135
|
+
def unstored_label_boxes(points, end_kind, stored_labels, connector)
|
|
130
136
|
return default_label_boxes(points, end_kind) unless stored_labels
|
|
137
|
+
return [{ "ox" => 0, "oy" => 0 }, { "ox" => 0, "oy" => 0 }] if connector.style.to_h[:phantom_outline] == true
|
|
131
138
|
|
|
132
139
|
[{ "ox" => 0, "oy" => 0, "alignment" => 0 }, { "ox" => 0, "oy" => 0, "alignment" => 0 }]
|
|
133
140
|
end
|
|
@@ -32,14 +32,11 @@ module Ea
|
|
|
32
32
|
@document = document
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Emitted once per sequencer: the canvas, diagram and options
|
|
36
|
+
# are fixed at construction, so a re-layout builds a new
|
|
37
|
+
# sequencer rather than re-emitting this one.
|
|
35
38
|
def layers
|
|
36
|
-
layers
|
|
37
|
-
layers += frame_layers if frame_enabled
|
|
38
|
-
layers += element_layers
|
|
39
|
-
layers += connector_layers
|
|
40
|
-
layers << labels_layer
|
|
41
|
-
layers << ghost_labels_layer
|
|
42
|
-
layers
|
|
39
|
+
@layers ||= build_layers.freeze
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
# Final drawn Bounds per model element ref (grown heights,
|
|
@@ -67,6 +64,16 @@ module Ea
|
|
|
67
64
|
|
|
68
65
|
private
|
|
69
66
|
|
|
67
|
+
def build_layers
|
|
68
|
+
layers = [Background.render(canvas)]
|
|
69
|
+
layers += frame_layers if frame_enabled
|
|
70
|
+
layers += element_layers
|
|
71
|
+
layers += connector_layers
|
|
72
|
+
layers << labels_layer
|
|
73
|
+
layers << ghost_labels_layer
|
|
74
|
+
layers
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
def frame_layers
|
|
71
78
|
DiagramFrame.new(canvas: canvas, theme: theme).layers(diagram).map(&:to_svg)
|
|
72
79
|
end
|
|
@@ -7,8 +7,9 @@ module Ea
|
|
|
7
7
|
# start element's placement row, then the connector id: the
|
|
8
8
|
# `paint_order` key the diagram builders store. Verified without a
|
|
9
9
|
# single inversion over 12785 connector pairs of the QEA corpus.
|
|
10
|
-
# Connectors with an unresolvable key
|
|
11
|
-
# phantoms (no stored key) come last, each
|
|
10
|
+
# Connectors with an unresolvable key (empty, or with a nil part)
|
|
11
|
+
# follow the keyed ones, and phantoms (no stored key) come last, each
|
|
12
|
+
# in link order.
|
|
12
13
|
module PaintOrder
|
|
13
14
|
module_function
|
|
14
15
|
|
|
@@ -18,9 +19,9 @@ module Ea
|
|
|
18
19
|
rank = if order.nil?
|
|
19
20
|
2
|
|
20
21
|
else
|
|
21
|
-
order.empty? ? 1 : 0
|
|
22
|
+
order.empty? || order.any?(&:nil?) ? 1 : 0
|
|
22
23
|
end
|
|
23
|
-
[rank, order
|
|
24
|
+
[rank, rank.zero? ? order : [], index]
|
|
24
25
|
end.map(&:first)
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# The [cap, join] stroke pen a drawing leaves behind. EA never sets a
|
|
7
|
+
# pen on notes and legends: they take whatever the previous drawing
|
|
8
|
+
# left, so the emitter threads the pen from one element to the next.
|
|
9
|
+
module Pen
|
|
10
|
+
ROUND_BEVEL = %w[round bevel].freeze
|
|
11
|
+
SQUARE_BEVEL = %w[square bevel].freeze
|
|
12
|
+
SQUARE_MITER = %w[square miter].freeze
|
|
13
|
+
|
|
14
|
+
# The pen in force when nothing has been drawn yet.
|
|
15
|
+
INITIAL = SQUARE_MITER
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -19,7 +19,7 @@ module Ea
|
|
|
19
19
|
:enum_literals, :tagged_values, :constraints, :marker_lines,
|
|
20
20
|
:package_content_lines,
|
|
21
21
|
:geometry, :theme, :canvas, :diagram, :model_index,
|
|
22
|
-
:off_canvas_parent_name, :
|
|
22
|
+
:off_canvas_parent_name, :previous_pen,
|
|
23
23
|
keyword_init: true
|
|
24
24
|
)
|
|
25
25
|
def note_body
|
|
@@ -24,14 +24,15 @@ module Ea
|
|
|
24
24
|
|
|
25
25
|
attr_reader :content, :x, :y, :family, :size, :weight, :style,
|
|
26
26
|
:fill, :text_length, :rotation, :size_unit,
|
|
27
|
-
:stroke_in_text, :width_factor, :logical_height
|
|
27
|
+
:stroke_in_text, :width_factor, :logical_height, :legend_title
|
|
28
28
|
|
|
29
29
|
def initialize(content:, x:, y:, family:, size:, weight: 400,
|
|
30
30
|
style: "normal", fill: DEFAULT_FILL,
|
|
31
31
|
text_length: nil, rotation: DEFAULT_ROTATION,
|
|
32
32
|
size_unit: "px", logical_height: nil,
|
|
33
33
|
stroke_in_text: DEFAULT_STROKE_IN_TEXT,
|
|
34
|
-
width_factor: DEFAULT_WIDTH_FACTOR)
|
|
34
|
+
width_factor: DEFAULT_WIDTH_FACTOR, legend_title: false)
|
|
35
|
+
@legend_title = legend_title
|
|
35
36
|
@content = content.to_s
|
|
36
37
|
@x = x
|
|
37
38
|
@y = y
|
|
@@ -155,23 +156,38 @@ module Ea
|
|
|
155
156
|
GLYPH_FALLBACK = 0.61
|
|
156
157
|
|
|
157
158
|
def self.estimate_width(text, size, _width_factor = nil, family: nil,
|
|
158
|
-
weight: nil, style: nil, logical_height: nil
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
weight: nil, style: nil, logical_height: nil,
|
|
160
|
+
legend_title: false)
|
|
161
|
+
opts = { family: family, weight: weight, style: style,
|
|
162
|
+
logical_height: logical_height, legend_title: legend_title }
|
|
163
|
+
real = Ea::Fonts::Metrics.text_length(text, size, **opts)
|
|
162
164
|
return real if real
|
|
165
|
+
return piecewise_width(text, size, opts) if Ea::Fonts::Metrics.piecewise_family?(family, size)
|
|
163
166
|
|
|
167
|
+
text.to_s.each_char.sum { |ch| glyph_estimate(ch, size) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A Cambria run outside 7pt that is not wholly in the measured
|
|
171
|
+
# table: table characters are exact, the rest are estimated one
|
|
172
|
+
# at a time with the glyph fallback, so the
|
|
173
|
+
# width never shrinks when a character is appended.
|
|
174
|
+
def self.piecewise_width(text, size, opts)
|
|
164
175
|
text.to_s.each_char.sum do |ch|
|
|
165
|
-
(
|
|
176
|
+
Ea::Fonts::Metrics.text_length(ch, size, **opts) || glyph_estimate(ch, size)
|
|
166
177
|
end
|
|
167
178
|
end
|
|
168
179
|
|
|
180
|
+
def self.glyph_estimate(char, size)
|
|
181
|
+
(GLYPH_WIDTHS[char] || GLYPH_FALLBACK) * size.to_f
|
|
182
|
+
end
|
|
183
|
+
|
|
169
184
|
def formatted_text_length
|
|
170
185
|
return text_length.to_i.to_s if text_length
|
|
171
186
|
|
|
172
187
|
width = self.class.estimate_width(content, size, width_factor,
|
|
173
188
|
family: family, weight: weight,
|
|
174
|
-
style: style, logical_height: logical_height
|
|
189
|
+
style: style, logical_height: logical_height,
|
|
190
|
+
legend_title: legend_title)
|
|
175
191
|
width.round.to_s
|
|
176
192
|
end
|
|
177
193
|
|
data/lib/ea/svg/ea_emitter.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Ea
|
|
|
32
32
|
autoload :Background, "ea/svg/ea_emitter/background"
|
|
33
33
|
autoload :BoundsCalculator, "ea/svg/ea_emitter/bounds_calculator"
|
|
34
34
|
autoload :Style, "ea/svg/ea_emitter/style"
|
|
35
|
+
autoload :Pen, "ea/svg/ea_emitter/pen"
|
|
35
36
|
autoload :Elements, "ea/svg/ea_emitter/elements"
|
|
36
37
|
autoload :PaintOrder, "ea/svg/ea_emitter/paint_order"
|
|
37
38
|
autoload :Connectors, "ea/svg/ea_emitter/connectors"
|
data/lib/ea/svg/parity/source.rb
CHANGED
data/lib/ea/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ea
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.141
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -982,6 +982,7 @@ files:
|
|
|
982
982
|
- lib/ea/svg/ea_emitter/markers.rb
|
|
983
983
|
- lib/ea/svg/ea_emitter/orthogonal_ray.rb
|
|
984
984
|
- lib/ea/svg/ea_emitter/paint_order.rb
|
|
985
|
+
- lib/ea/svg/ea_emitter/pen.rb
|
|
985
986
|
- lib/ea/svg/ea_emitter/render_context.rb
|
|
986
987
|
- lib/ea/svg/ea_emitter/segment_geometry.rb
|
|
987
988
|
- lib/ea/svg/ea_emitter/style.rb
|