ea 0.6.89 → 0.6.91
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/sources/qea/diagram_builder/routing.rb +101 -0
- data/lib/ea/sources/qea/diagram_builder.rb +58 -99
- data/lib/ea/svg/ea_emitter/connector_redock.rb +57 -20
- data/lib/ea/svg/ea_emitter/label/end_label.rb +14 -3
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +30 -1
- data/lib/ea/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf0f3e11888e4492386de976f97e4024e39d9dbc95083a5b184c0fb25d8cfead
|
|
4
|
+
data.tar.gz: 467687628f21bf50264c0210de75e2fad5013a14d7f2a1572d65dfb218429052
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67a177d6e826985a3526c63bae7ed1f10e55b51ca97ec1507e5cb2f89f309b789f734c27cd41b678777e3ea72cb730a44485799972c3bbab916dedfd51f404c5
|
|
7
|
+
data.tar.gz: 55d5d873894f3e26fb786a71efaab326ecd4df2d16d167bf3dc7158e1775e139f9a9c2096cd79c5ae136315421c1d19156320d1c895837d691ecd7a97e772a2a
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Qea
|
|
6
|
+
class DiagramBuilder
|
|
7
|
+
# Internal geometry for choosing the facing edges of stored routes.
|
|
8
|
+
module Routing
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Keep a vertical tree trunk whose bends still fit both boxes
|
|
12
|
+
# and the open gap; the closest facing edges need not share its x.
|
|
13
|
+
def preserve_stored_tree?(points, link_row, connector, src, tgt)
|
|
14
|
+
return false unless connector.connector_type == "Aggregation" && points.size == 4
|
|
15
|
+
|
|
16
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
17
|
+
return false unless style[:tree] == "V" && style[:lwidth] == "2"
|
|
18
|
+
|
|
19
|
+
tree_bends_fit?(points[1], points[-2], src, tgt)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tree_bends_fit?(first, last, source, target)
|
|
23
|
+
first[0] != last[0] && first[1] == last[1] &&
|
|
24
|
+
within_vertical_gap?(first[1], source, target) &&
|
|
25
|
+
within_horizontal_span?(first, source) && within_horizontal_span?(last, target)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def within_vertical_gap?(height, source, target)
|
|
29
|
+
height < source.y - 1 && height > target.y + target.height + 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def within_horizontal_span?(point, bounds)
|
|
33
|
+
point[0].between?(bounds.x, bounds.x + bounds.width)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns a replacement 2-point route between the closest
|
|
37
|
+
# facing edges, or the original points when: the boxes overlap
|
|
38
|
+
# on both axes, the current route already docks those edges,
|
|
39
|
+
# or no aligned dock fits the edges' overlap range.
|
|
40
|
+
def facing_edge_route(points, src, tgt)
|
|
41
|
+
v_gap = v_dir = h_gap = h_dir = nil
|
|
42
|
+
if src.y >= tgt.y + tgt.height
|
|
43
|
+
v_gap = src.y - (tgt.y + tgt.height)
|
|
44
|
+
v_dir = [:t, :b]
|
|
45
|
+
elsif tgt.y >= src.y + src.height
|
|
46
|
+
v_gap = tgt.y - (src.y + src.height)
|
|
47
|
+
v_dir = [:b, :t]
|
|
48
|
+
end
|
|
49
|
+
if src.x >= tgt.x + tgt.width
|
|
50
|
+
h_gap = src.x - (tgt.x + tgt.width)
|
|
51
|
+
h_dir = [:l, :r]
|
|
52
|
+
elsif tgt.x >= src.x + src.width
|
|
53
|
+
h_gap = tgt.x - (tgt.x + tgt.width)
|
|
54
|
+
h_dir = [:r, :l]
|
|
55
|
+
end
|
|
56
|
+
return points if v_gap.nil? && h_gap.nil?
|
|
57
|
+
|
|
58
|
+
if !v_gap.nil? && (h_gap.nil? || v_gap <= h_gap)
|
|
59
|
+
src_edge, tgt_edge = v_dir
|
|
60
|
+
else
|
|
61
|
+
src_edge, tgt_edge = h_dir
|
|
62
|
+
end
|
|
63
|
+
return points if docks_edges?(points, src, src_edge, tgt, tgt_edge)
|
|
64
|
+
|
|
65
|
+
if %i[t b].include?(src_edge)
|
|
66
|
+
lo = [src.x, tgt.x].max
|
|
67
|
+
hi = [src.x + src.width, tgt.x + tgt.width].min
|
|
68
|
+
return points if lo > hi
|
|
69
|
+
|
|
70
|
+
x = points.first[0].clamp(lo, hi)
|
|
71
|
+
[[x, src_edge == :t ? src.y : src.y + src.height],
|
|
72
|
+
[x, tgt_edge == :t ? tgt.y : tgt.y + tgt.height]]
|
|
73
|
+
else
|
|
74
|
+
lo = [src.y, tgt.y].max
|
|
75
|
+
hi = [src.y + src.height, tgt.y + tgt.height].min
|
|
76
|
+
return points if lo > hi
|
|
77
|
+
|
|
78
|
+
y = points.first[1].clamp(lo, hi)
|
|
79
|
+
[[src_edge == :l ? src.x : src.x + src.width, y],
|
|
80
|
+
[tgt_edge == :l ? tgt.x : tgt.x + tgt.width, y]]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def docks_edges?(points, src, src_edge, tgt, tgt_edge)
|
|
85
|
+
on_edge?(points.first, src, src_edge) && on_edge?(points.last, tgt, tgt_edge)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def on_edge?(pt, b, edge, tol = 2)
|
|
89
|
+
x, y = pt
|
|
90
|
+
case edge
|
|
91
|
+
when :l then (x - b.x).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
|
|
92
|
+
when :r then (x - (b.x + b.width)).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
|
|
93
|
+
when :t then (y - b.y).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
|
|
94
|
+
else (y - (b.y + b.height)).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -10,6 +10,10 @@ module Ea
|
|
|
10
10
|
# DiagramConnectors (including pixel coordinates and parsed
|
|
11
11
|
# styles).
|
|
12
12
|
class DiagramBuilder
|
|
13
|
+
autoload :Routing, "ea/sources/qea/diagram_builder/routing"
|
|
14
|
+
include Routing
|
|
15
|
+
private_constant :Routing
|
|
16
|
+
|
|
13
17
|
attr_reader :database
|
|
14
18
|
|
|
15
19
|
# Generalizations whose EA CustomProperties carry
|
|
@@ -605,21 +609,15 @@ module Ea
|
|
|
605
609
|
intermediate = intermediate_waypoints(link_row)
|
|
606
610
|
if edge_out == 2 && source_port && target_port &&
|
|
607
611
|
!valid_tree_path?(intermediate, source_placement)
|
|
608
|
-
#
|
|
609
|
-
#
|
|
610
|
-
#
|
|
611
|
-
# stale Path vs EA-drawn route; CityObjectGroup's current
|
|
612
|
-
# Path renders byte-exact and is kept). Computed route:
|
|
613
|
-
# source port -> corner at (target dock x, source exit
|
|
614
|
-
# y) -> target port.
|
|
612
|
+
# A stale tree Path (not on the source's facing edge) is
|
|
613
|
+
# recomputed: source port -> corner at (target dock x,
|
|
614
|
+
# source exit y) -> target port.
|
|
615
615
|
points = [[source_point[0], source_point[1]],
|
|
616
616
|
[target_point[0], source_point[1]],
|
|
617
617
|
[target_point[0], target_point[1]]]
|
|
618
618
|
elsif edge_out == 2 && intermediate.size == 1
|
|
619
|
-
# Current tree Path
|
|
620
|
-
#
|
|
621
|
-
# bend's y, corner at the bend, target facing edge at the
|
|
622
|
-
# bend's x (byte-exact on CityObjectGroup).
|
|
619
|
+
# Current tree Path: source right edge at the bend's y,
|
|
620
|
+
# corner at the bend, target facing edge at the bend's x.
|
|
623
621
|
src = bounds_from_rect(source_placement)
|
|
624
622
|
tgt = bounds_from_rect(target_placement)
|
|
625
623
|
bend = intermediate.first
|
|
@@ -630,21 +628,11 @@ module Ea
|
|
|
630
628
|
[bend[0], target_y]]
|
|
631
629
|
elsif intermediate.any?
|
|
632
630
|
points = [source_point, *intermediate, target_point]
|
|
633
|
-
# Generalizations
|
|
634
|
-
#
|
|
635
|
-
# the box edges AT THE ADJACENT BEND's x (hand-verified:
|
|
636
|
-
# stored (542,975)(541,807)(543,807)(486,757) draws as
|
|
637
|
-
# (541,975)(541,807)(543,807)(543,757) — only the two
|
|
638
|
-
# endpoints move; the stale final x is replaced by the
|
|
639
|
-
# last bend's trunk x).
|
|
631
|
+
# Parent-above Generalizations keep stored interior bends
|
|
632
|
+
# and re-dock only the endpoints, at the adjacent bend's x.
|
|
640
633
|
if connector.connector_type == "Generalization"
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if tgt_b.y + tgt_b.height <= src_b.y
|
|
644
|
-
points[0] = [points[1][0].clamp(src_b.x, src_b.x + src_b.width), src_b.y]
|
|
645
|
-
points[-1] = [points[-2][0].clamp(tgt_b.x, tgt_b.x + tgt_b.width),
|
|
646
|
-
tgt_b.y + tgt_b.height]
|
|
647
|
-
end
|
|
634
|
+
redock_parent_above(points, link_row, source_placement,
|
|
635
|
+
target_placement)
|
|
648
636
|
end
|
|
649
637
|
elsif source_port || target_port
|
|
650
638
|
# Exact ports are known — the SX/SY bend heuristics in
|
|
@@ -654,20 +642,24 @@ module Ea
|
|
|
654
642
|
points = sx_sy_ex_ey_waypoints(source_point, target_point, geom)
|
|
655
643
|
end
|
|
656
644
|
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
#
|
|
660
|
-
# edges (corpus: edge pair = min facing gap, 85% accurate;
|
|
661
|
-
# dock coordinate = the stored route's own coordinate
|
|
662
|
-
# clamped into the edges' overlap; simulation on 417
|
|
663
|
-
# identity-paired connectors: exact routes 41 -> 100).
|
|
664
|
-
# Tree-style (EDGE=2) connectors are excluded — their
|
|
665
|
-
# computed routes are already byte-exact.
|
|
645
|
+
# Between DISJOINT boxes, a stored route that does not dock the
|
|
646
|
+
# closest facing edge pair is redrawn straight between them.
|
|
647
|
+
# EDGE=2 routes are excluded; they are already exact.
|
|
666
648
|
if %w[Association Aggregation].include?(connector.connector_type) &&
|
|
667
649
|
edge_out != 2
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
650
|
+
src = bounds_from_rect(source_placement)
|
|
651
|
+
tgt = bounds_from_rect(target_placement)
|
|
652
|
+
unless preserve_stored_tree?(points, link_row, connector, src, tgt)
|
|
653
|
+
points = facing_edge_route(points, src, tgt)
|
|
654
|
+
end
|
|
655
|
+
# A width-2 Aggregation that keeps its stored bends is
|
|
656
|
+
# docked like a Generalization one.
|
|
657
|
+
if connector.connector_type == "Aggregation" &&
|
|
658
|
+
intermediate.any? && points.size >= 4 &&
|
|
659
|
+
generalization_outline_offset(link_row, points.size) == 1
|
|
660
|
+
redock_parent_above(points, link_row, source_placement,
|
|
661
|
+
target_placement, strict: true)
|
|
662
|
+
end
|
|
671
663
|
end
|
|
672
664
|
|
|
673
665
|
points.map do |x, y|
|
|
@@ -675,68 +667,6 @@ module Ea
|
|
|
675
667
|
end
|
|
676
668
|
end
|
|
677
669
|
|
|
678
|
-
# Returns a replacement 2-point route between the closest
|
|
679
|
-
# facing edges, or the original points when: the boxes overlap
|
|
680
|
-
# on both axes, the current route already docks those edges,
|
|
681
|
-
# or no aligned dock fits the edges' overlap range.
|
|
682
|
-
def facing_edge_route(points, src, tgt)
|
|
683
|
-
v_gap = v_dir = h_gap = h_dir = nil
|
|
684
|
-
if src.y >= tgt.y + tgt.height
|
|
685
|
-
v_gap = src.y - (tgt.y + tgt.height)
|
|
686
|
-
v_dir = [:t, :b]
|
|
687
|
-
elsif tgt.y >= src.y + src.height
|
|
688
|
-
v_gap = tgt.y - (src.y + src.height)
|
|
689
|
-
v_dir = [:b, :t]
|
|
690
|
-
end
|
|
691
|
-
if src.x >= tgt.x + tgt.width
|
|
692
|
-
h_gap = src.x - (tgt.x + tgt.width)
|
|
693
|
-
h_dir = [:l, :r]
|
|
694
|
-
elsif tgt.x >= src.x + src.width
|
|
695
|
-
h_gap = tgt.x - (tgt.x + tgt.width)
|
|
696
|
-
h_dir = [:r, :l]
|
|
697
|
-
end
|
|
698
|
-
return points if v_gap.nil? && h_gap.nil?
|
|
699
|
-
|
|
700
|
-
if !v_gap.nil? && (h_gap.nil? || v_gap <= h_gap)
|
|
701
|
-
src_edge, tgt_edge = v_dir
|
|
702
|
-
else
|
|
703
|
-
src_edge, tgt_edge = h_dir
|
|
704
|
-
end
|
|
705
|
-
return points if docks_edges?(points, src, src_edge, tgt, tgt_edge)
|
|
706
|
-
|
|
707
|
-
if %i[t b].include?(src_edge)
|
|
708
|
-
lo = [src.x, tgt.x].max
|
|
709
|
-
hi = [src.x + src.width, tgt.x + tgt.width].min
|
|
710
|
-
return points if lo > hi
|
|
711
|
-
|
|
712
|
-
x = points.first[0].clamp(lo, hi)
|
|
713
|
-
[[x, src_edge == :t ? src.y : src.y + src.height],
|
|
714
|
-
[x, tgt_edge == :t ? tgt.y : tgt.y + tgt.height]]
|
|
715
|
-
else
|
|
716
|
-
lo = [src.y, tgt.y].max
|
|
717
|
-
hi = [src.y + src.height, tgt.y + tgt.height].min
|
|
718
|
-
return points if lo > hi
|
|
719
|
-
|
|
720
|
-
y = points.first[1].clamp(lo, hi)
|
|
721
|
-
[[src_edge == :l ? src.x : src.x + src.width, y],
|
|
722
|
-
[tgt_edge == :l ? tgt.x : tgt.x + tgt.width, y]]
|
|
723
|
-
end
|
|
724
|
-
end
|
|
725
|
-
|
|
726
|
-
def docks_edges?(points, src, src_edge, tgt, tgt_edge)
|
|
727
|
-
on_edge?(points.first, src, src_edge) && on_edge?(points.last, tgt, tgt_edge)
|
|
728
|
-
end
|
|
729
|
-
|
|
730
|
-
def on_edge?(pt, b, edge, tol = 2)
|
|
731
|
-
x, y = pt
|
|
732
|
-
case edge
|
|
733
|
-
when :l then (x - b.x).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
|
|
734
|
-
when :r then (x - (b.x + b.width)).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
|
|
735
|
-
when :t then (y - b.y).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
|
|
736
|
-
else (y - (b.y + b.height)).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
|
|
737
|
-
end
|
|
738
|
-
end
|
|
739
|
-
|
|
740
670
|
# A tree Path is current when its bend height still falls
|
|
741
671
|
# within the source element's y-range (the exit height EA
|
|
742
672
|
# records); a bend outside the element is stale and EA
|
|
@@ -978,6 +908,35 @@ module Ea
|
|
|
978
908
|
|
|
979
909
|
IdNormalizer.from_guid(conn.ea_guid)
|
|
980
910
|
end
|
|
911
|
+
|
|
912
|
+
# Docks both ends of a bent route on the box edges at the
|
|
913
|
+
# adjacent bend's x when the target is above the source; interior
|
|
914
|
+
# bends stay. strict: skip an end whose bend is not beyond its dock.
|
|
915
|
+
def redock_parent_above(points, link_row, source_placement,
|
|
916
|
+
target_placement, strict: false)
|
|
917
|
+
src_b = bounds_from_rect(source_placement)
|
|
918
|
+
tgt_b = bounds_from_rect(target_placement)
|
|
919
|
+
return points unless tgt_b.y + tgt_b.height <= src_b.y
|
|
920
|
+
|
|
921
|
+
inset = generalization_outline_offset(link_row, points.size)
|
|
922
|
+
source_y = src_b.y
|
|
923
|
+
target_y = tgt_b.y + tgt_b.height
|
|
924
|
+
source_y -= inset if points[1][1] < source_y - inset
|
|
925
|
+
target_y += inset if points[-2][1] > target_y + inset
|
|
926
|
+
unless strict && points[1][1] >= source_y
|
|
927
|
+
points[0] = [points[1][0].clamp(src_b.x, src_b.x + src_b.width), source_y]
|
|
928
|
+
end
|
|
929
|
+
unless strict && points[-2][1] <= target_y
|
|
930
|
+
points[-1] = [points[-2][0].clamp(tgt_b.x, tgt_b.x + tgt_b.width), target_y]
|
|
931
|
+
end
|
|
932
|
+
points
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def generalization_outline_offset(link_row, point_count)
|
|
936
|
+
return 0 if point_count < 4
|
|
937
|
+
|
|
938
|
+
DiagramStyleParser.parse(link_row.style)[:lwidth] == "2" ? 1 : 0
|
|
939
|
+
end
|
|
981
940
|
end
|
|
982
941
|
end
|
|
983
942
|
end
|
|
@@ -36,16 +36,45 @@ module Ea
|
|
|
36
36
|
return nil unless source_bounds && target_bounds
|
|
37
37
|
return nil if source_ref == target_ref
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
anchors = [connector.source_anchor, connector.target_anchor]
|
|
40
|
+
bounds = [source_bounds, target_bounds]
|
|
41
|
+
original = clipped_pairs(bounds, anchors)
|
|
42
|
+
return original unless stroke_outline?(connector, bounds)
|
|
43
|
+
|
|
44
|
+
# EA docks these markers on the outer edge of the 2px box stroke.
|
|
45
|
+
outline = bounds.map do |box|
|
|
46
|
+
Ea::Model::Bounds.new(x: box.x - 1, y: box.y - 1,
|
|
47
|
+
width: box.width + 2, height: box.height + 2)
|
|
48
|
+
end
|
|
49
|
+
candidate = clipped_pairs(outline, anchors)
|
|
50
|
+
forward_route?(original, candidate) ? candidate : original
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stroke_outline?(connector, bounds)
|
|
54
|
+
connector.style.to_h[:lwidth] == "2" &&
|
|
55
|
+
%w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
56
|
+
bounds.all? { |box| box.width.positive? && box.height.positive? }
|
|
57
|
+
end
|
|
58
|
+
private_class_method :stroke_outline?
|
|
59
|
+
|
|
60
|
+
def clipped_pairs(bounds, anchors)
|
|
61
|
+
source, target = bounds
|
|
62
|
+
source_anchor, target_anchor = anchors
|
|
63
|
+
if source_anchor && target_anchor
|
|
64
|
+
[anchor_exit(source_anchor, source, target_anchor),
|
|
65
|
+
anchor_exit(target_anchor, target, source_anchor)]
|
|
44
66
|
else
|
|
45
|
-
[edge_point(
|
|
46
|
-
edge_point(target_bounds, source_bounds)]
|
|
67
|
+
[edge_point(source, target), edge_point(target, source)]
|
|
47
68
|
end
|
|
48
69
|
end
|
|
70
|
+
private_class_method :clipped_pairs
|
|
71
|
+
|
|
72
|
+
def forward_route?(original, candidate)
|
|
73
|
+
before = original.first.zip(original.last).map { |start, finish| finish - start }
|
|
74
|
+
after = candidate.first.zip(candidate.last).map { |start, finish| finish - start }
|
|
75
|
+
before.zip(after).sum { |old, current| old * current }.positive?
|
|
76
|
+
end
|
|
77
|
+
private_class_method :forward_route?
|
|
49
78
|
|
|
50
79
|
# Exit point of the anchor-to-anchor segment through a box's
|
|
51
80
|
# outline: smallest positive t whose crossing lies on the
|
|
@@ -101,21 +130,29 @@ module Ea
|
|
|
101
130
|
divider_y_by_ref[target_ref]]
|
|
102
131
|
return nil unless bounds.all? && dividers.all?
|
|
103
132
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
adjusted = row_slot_pairs(pairs, bounds, dividers)
|
|
134
|
+
if stroke_outline?(connector, bounds) &&
|
|
135
|
+
pairs == pairs_for(connector, diagram, model_index, bounds_map)
|
|
136
|
+
original = clipped_pairs(bounds, [connector.source_anchor, connector.target_anchor])
|
|
137
|
+
original_final = row_slot_pairs(original, bounds, dividers)
|
|
138
|
+
adjusted = original_final if original != pairs && !forward_route?(original_final, adjusted)
|
|
139
|
+
end
|
|
140
|
+
adjusted == pairs ? nil : adjusted
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def row_slot_pairs(pairs, bounds, dividers)
|
|
144
|
+
pairs.each_with_index.map do |(x, y), index|
|
|
145
|
+
row_slot?(x, y, bounds[index], dividers[index]) ? [x, y + ROW_SLOT_PITCH] : [x, y]
|
|
116
146
|
end
|
|
117
|
-
changed ? adjusted : nil
|
|
118
147
|
end
|
|
148
|
+
private_class_method :row_slot_pairs
|
|
149
|
+
|
|
150
|
+
def row_slot?(x_position, y_position, bounds, divider)
|
|
151
|
+
at_edge = (x_position - bounds.x).abs <= 1 ||
|
|
152
|
+
(x_position - (bounds.x + bounds.width)).abs <= 1
|
|
153
|
+
at_edge && (y_position - divider.to_f).abs <= 1
|
|
154
|
+
end
|
|
155
|
+
private_class_method :row_slot?
|
|
119
156
|
|
|
120
157
|
def direct?(connector)
|
|
121
158
|
style = connector.style || {}
|
|
@@ -75,7 +75,7 @@ module Ea
|
|
|
75
75
|
stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
|
|
76
76
|
|
|
77
77
|
build_texts(role:, mult:, standalone:,
|
|
78
|
-
text_pos:, mult_pos:, stereo:)
|
|
78
|
+
text_pos:, mult_pos:, stereo:, text_box:)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
private
|
|
@@ -110,12 +110,13 @@ module Ea
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def build_texts(role:, mult:, standalone:, text_pos:, mult_pos:,
|
|
113
|
-
stereo:)
|
|
113
|
+
stereo:, text_box:)
|
|
114
114
|
if role && !role.empty? && text_pos
|
|
115
115
|
x, y = text_pos
|
|
116
116
|
texts = [text_at(x, y, role)]
|
|
117
117
|
if stereo
|
|
118
|
-
|
|
118
|
+
stereo_x = x + stereotype_offset(role, stereo, text_box)
|
|
119
|
+
texts << text_at(stereo_x, y + property_y_offset, stereo)
|
|
119
120
|
end
|
|
120
121
|
if mult && !mult.empty? && mult_pos
|
|
121
122
|
texts << text_at(*mult_pos, mult)
|
|
@@ -128,6 +129,16 @@ module Ea
|
|
|
128
129
|
end
|
|
129
130
|
end
|
|
130
131
|
|
|
132
|
+
def stereotype_offset(role, stereo, text_box)
|
|
133
|
+
return 0 unless text_box && text_box["alignment"] == 1
|
|
134
|
+
|
|
135
|
+
role_width = TextRenderer.estimate_width(role, font_size,
|
|
136
|
+
family: font_family).round
|
|
137
|
+
stereo_width = TextRenderer.estimate_width(stereo, font_size,
|
|
138
|
+
family: font_family).round
|
|
139
|
+
((role_width - stereo_width) / 2.0).round
|
|
140
|
+
end
|
|
141
|
+
|
|
131
142
|
# EA renders «Property» between role name and multiplicity
|
|
132
143
|
# exactly on connector ends carrying
|
|
133
144
|
# t_connector.SourceStereotype / DestStereotype = "Property"
|
|
@@ -32,7 +32,7 @@ module Ea
|
|
|
32
32
|
content = midpoint_content(connector)
|
|
33
33
|
return nil if content.nil?
|
|
34
34
|
|
|
35
|
-
mx, my =
|
|
35
|
+
mx, my = label_position(connector, points)
|
|
36
36
|
# EA splits a "name: value" label at the colon-space into
|
|
37
37
|
# two lines centered on the same slot (81F92FC7: "new
|
|
38
38
|
# ownership:" / "LA_Right" at y 172/185, both centered on
|
|
@@ -95,6 +95,35 @@ module Ea
|
|
|
95
95
|
|
|
96
96
|
LINE_PITCH = 13
|
|
97
97
|
|
|
98
|
+
def label_position(connector, points)
|
|
99
|
+
return midpoint(points) unless default_substitutable_seat?(connector) &&
|
|
100
|
+
upward_four_point_route?(points)
|
|
101
|
+
|
|
102
|
+
source, bend = points
|
|
103
|
+
[source[0] + 3, ((source[1] + bend[1]) / 2).floor - 1]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def default_substitutable_seat?(connector)
|
|
107
|
+
connector.is_a?(Ea::Model::DiagramConnector) && connector.is_substitutable &&
|
|
108
|
+
connector.connector_type == "Generalization" && font_size == 7 &&
|
|
109
|
+
stereotype_label(connector).nil? &&
|
|
110
|
+
default_constraint_box?(connector.label_boxes.to_h[:lmb])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def default_constraint_box?(box)
|
|
114
|
+
box && !box["hidden"] &&
|
|
115
|
+
box.values_at("ox", "oy", "rotation", "alignment") == [0, 0, 0, 0]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def upward_four_point_route?(points)
|
|
119
|
+
return false unless points.size == 4
|
|
120
|
+
|
|
121
|
+
source, first_bend, last_bend, target = points
|
|
122
|
+
source[0] == first_bend[0] && first_bend[1] == last_bend[1] &&
|
|
123
|
+
last_bend[0] == target[0] && source[1] > first_bend[1] &&
|
|
124
|
+
target[1] < last_bend[1]
|
|
125
|
+
end
|
|
126
|
+
|
|
98
127
|
def midpoint(points)
|
|
99
128
|
mid_idx = points.size / 2
|
|
100
129
|
if points.size.odd?
|
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.91
|
|
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-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -815,6 +815,7 @@ files:
|
|
|
815
815
|
- lib/ea/sources/qea/connector_relationship_map.rb
|
|
816
816
|
- lib/ea/sources/qea/context_diagram_support.rb
|
|
817
817
|
- lib/ea/sources/qea/diagram_builder.rb
|
|
818
|
+
- lib/ea/sources/qea/diagram_builder/routing.rb
|
|
818
819
|
- lib/ea/sources/qea/diagram_style_parser.rb
|
|
819
820
|
- lib/ea/sources/qea/id_normalizer.rb
|
|
820
821
|
- lib/ea/sources/qea/instance_builder.rb
|