ea 0.6.138 → 0.6.139
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/xmi/diagram_builder.rb +3 -1
- data/lib/ea/sources/xmi/stored_route.rb +38 -2
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +38 -40
- data/lib/ea/svg/ea_emitter/segment_geometry.rb +123 -0
- data/lib/ea/svg/ea_emitter.rb +2 -0
- data/lib/ea/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34c3f680cb51a88a683e57db2c059fcee8979e5b6b31cfaae4c3cbc3ac91ac86
|
|
4
|
+
data.tar.gz: a4cbd25958c54c24d6d1801bf4e4cc83f76f42d652f371cf9d491865160d7725
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c87682bc64b73abb6502c0dfd71ca7b9209ab8bcd71b79762032d0c4008f80999e3785257b72f39d73a816247d1dbcc5580a70e64f526a8ee98ee1aab32b645a
|
|
7
|
+
data.tar.gz: ed3c4dd24e1ed8b1bd9c371545d56392cda07b2e0f3223f27d84761b728963c5df7df67fab8da905bdd586150fb94750a5cbe3f2bd1bec9fd11adfd3ec18b9c6
|
|
@@ -280,7 +280,9 @@ module Ea
|
|
|
280
280
|
|
|
281
281
|
metadata = stored_ray_metadata(connector, placed, source, target)
|
|
282
282
|
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
283
|
-
|
|
283
|
+
return if metadata.empty? || connector.connector_type == "Generalization"
|
|
284
|
+
|
|
285
|
+
connector.style[:association_ray_outline] = true
|
|
284
286
|
end
|
|
285
287
|
|
|
286
288
|
def stored_ray_metadata(connector, placed, source, target, stage: :ray_metadata)
|
|
@@ -26,9 +26,12 @@ module Ea
|
|
|
26
26
|
finish(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
# well as their computed boundary ports, just as the QEA builder does.
|
|
29
|
+
# Saved rays retain origins and computed boundary ports, matching QEA.
|
|
31
30
|
def ray_metadata(model, link:, bounds:, offsets:, edge:)
|
|
31
|
+
if model.connector_type == "Generalization"
|
|
32
|
+
return generalization_ray_metadata(model, link, bounds, offsets, edge)
|
|
33
|
+
end
|
|
34
|
+
|
|
32
35
|
return {} unless direct_association_ray?(model, bounds, offsets)
|
|
33
36
|
|
|
34
37
|
source, target = bounds.values_at(:source, :target)
|
|
@@ -169,6 +172,39 @@ module Ea
|
|
|
169
172
|
bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
170
173
|
end
|
|
171
174
|
|
|
175
|
+
# A direct Generalization clips its saved mathematical origins rather
|
|
176
|
+
# than treating boundary ports as the origins of the final drawn ray.
|
|
177
|
+
def generalization_ray_metadata(model, link, bounds, offsets, edge)
|
|
178
|
+
geom = ray_geometry(offsets, edge)
|
|
179
|
+
return {} unless generalization_ray_candidate?(model, link, bounds, offsets, geom)
|
|
180
|
+
|
|
181
|
+
source, target = bounds.values_at(:source, :target)
|
|
182
|
+
return {} unless stored_ray_gap?(source, target, model.style[:mode], edge)
|
|
183
|
+
|
|
184
|
+
points = ray_points(source, target, offsets, geom)
|
|
185
|
+
return {} if points.empty?
|
|
186
|
+
|
|
187
|
+
points.merge(style: generalization_ray_style(model, points, source, target))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def generalization_ray_candidate?(model, link, bounds, offsets, geom)
|
|
191
|
+
model.waypoints.size == 2 && stored_ray_style?(model.style, link) &&
|
|
192
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?) &&
|
|
193
|
+
(!geom[:sy].zero? || !geom[:ey].zero?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def generalization_ray_style(model, points, source, target)
|
|
197
|
+
candidate = Ea::Model::DiagramConnector.new(
|
|
198
|
+
connector_type: model.connector_type, style: model.style.to_h.dup,
|
|
199
|
+
source_anchor: points[:source_anchor], target_anchor: points[:target_anchor]
|
|
200
|
+
)
|
|
201
|
+
return model.style.to_h unless centered_origin_ray?(candidate, source, target)
|
|
202
|
+
|
|
203
|
+
flags = { centered_origin_ray: true }
|
|
204
|
+
flags[:stored_origin_ray] = true if stored_origin_ray?(candidate, source, target)
|
|
205
|
+
model.style.to_h.merge(flags)
|
|
206
|
+
end
|
|
207
|
+
|
|
172
208
|
def ray_geometry(offsets, edge)
|
|
173
209
|
{ sx: offsets[:source][0], sy: offsets[:source][1],
|
|
174
210
|
ex: offsets[:target][0], ey: offsets[:target][1], edge: edge }
|
|
@@ -9,6 +9,7 @@ module Ea
|
|
|
9
9
|
# connector passes over a crossing.
|
|
10
10
|
class ConnectorBridges
|
|
11
11
|
RADIUS = 6
|
|
12
|
+
PHANTOM = :phantom
|
|
12
13
|
|
|
13
14
|
def initialize(_diagram, routes)
|
|
14
15
|
@routes = routes.map { |connector, points| [connector, points, priority_for(connector)] }
|
|
@@ -56,78 +57,75 @@ module Ea
|
|
|
56
57
|
|
|
57
58
|
# nil or empty: no bridge decision for this connector.
|
|
58
59
|
def priority_for(connector)
|
|
60
|
+
return PHANTOM if phantom?(connector)
|
|
61
|
+
|
|
59
62
|
connector.paint_order if connector.paint_order&.any?
|
|
60
63
|
end
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
# A phantom (relationship without a link row) has no stored order; it
|
|
66
|
+
# is appended after every link in the export, so it passes over them.
|
|
67
|
+
def phantom?(connector)
|
|
68
|
+
connector.paint_order.nil?
|
|
69
|
+
end
|
|
65
70
|
|
|
71
|
+
def crossings_for(connector, priority, first, last)
|
|
66
72
|
crossings = lower_routes(connector, priority).flat_map do |_partner, points, _order|
|
|
67
73
|
points.each_cons(2).filter_map do |start, finish|
|
|
68
|
-
crossing(first, last, start, finish
|
|
74
|
+
crossing(first, last, start, finish)
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
|
-
separate_crossings(crossings,
|
|
77
|
+
separate_crossings(crossings, first, last).sort_by { |point| distance(point, first) }
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
# The order key is only proven across different start placements, so
|
|
75
81
|
# connectors leaving the same placement make no carrier decision.
|
|
76
82
|
def lower_routes(connector, priority)
|
|
83
|
+
return keyed_routes(connector) if priority == PHANTOM
|
|
84
|
+
|
|
77
85
|
@routes.select do |partner, _points, order|
|
|
78
|
-
!partner.equal?(connector) && order && order.size == priority.size &&
|
|
86
|
+
!partner.equal?(connector) && order.is_a?(Array) && order.size == priority.size &&
|
|
79
87
|
!same_source?(connector, partner) && (order <=> priority).negative?
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
def keyed_routes(connector)
|
|
92
|
+
@routes.select { |partner, _points, order| !partner.equal?(connector) && order.is_a?(Array) }
|
|
93
|
+
end
|
|
94
|
+
|
|
83
95
|
def same_source?(connector, partner)
|
|
84
96
|
connector.source_element_ref && connector.source_element_ref == partner.source_element_ref
|
|
85
97
|
end
|
|
86
98
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
# Partners crossing at one point share a single arc (0BE81AC5 draws one
|
|
100
|
+
# arc for three coincident verticals). Distinct crossings whose spans
|
|
101
|
+
# overlap are all suppressed so route order cannot select one of
|
|
102
|
+
# several ambiguous arcs.
|
|
103
|
+
def separate_crossings(crossings, first, last)
|
|
104
|
+
crossings = crossings.uniq { |point| point.map { |value| value.round(6) } }
|
|
105
|
+
axis = SegmentGeometry.dominant_axis(first, last)
|
|
106
|
+
spans = crossings.map do |point|
|
|
107
|
+
SegmentGeometry.chord(first, last, point, RADIUS).map { |seat| seat[axis] }.minmax
|
|
94
108
|
end
|
|
109
|
+
crossings.reject.with_index { |_point, index| overlapping_span?(spans, index) }
|
|
95
110
|
end
|
|
96
111
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
segment_interior?(point, start, finish, 1 - axis)
|
|
103
|
-
|
|
104
|
-
point
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def segment_interior?(point, first, last, axis)
|
|
108
|
-
interior?(point[axis], first[axis], last[axis])
|
|
112
|
+
def overlapping_span?(spans, index)
|
|
113
|
+
minimum, maximum = spans[index]
|
|
114
|
+
spans.each_with_index.any? do |(other_minimum, other_maximum), other_index|
|
|
115
|
+
index != other_index && minimum < other_maximum && other_minimum < maximum
|
|
116
|
+
end
|
|
109
117
|
end
|
|
110
118
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
value > minimum && value < maximum &&
|
|
114
|
-
value - minimum >= RADIUS && maximum - value >= RADIUS
|
|
119
|
+
def crossing(first, last, start, finish)
|
|
120
|
+
SegmentGeometry.crossing(first, last, start, finish, RADIUS)
|
|
115
121
|
end
|
|
116
122
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
return 1 if first[0] == last[0] && first[1] != last[1]
|
|
120
|
-
|
|
121
|
-
nil
|
|
123
|
+
def distance(point, other)
|
|
124
|
+
SegmentGeometry.distance(point, other)
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def arc_commands(first, last, crossing)
|
|
125
|
-
|
|
126
|
-
direction = last[axis] > first[axis] ? 1 : -1
|
|
127
|
-
start = crossing.dup
|
|
128
|
-
finish = crossing.dup
|
|
129
|
-
start[axis] -= direction * RADIUS
|
|
130
|
-
finish[axis] += direction * RADIUS
|
|
128
|
+
start, finish = SegmentGeometry.chord(first, last, crossing, RADIUS)
|
|
131
129
|
["L #{coordinates(start)}", "A #{RADIUS} #{RADIUS} 180 0 0 #{coordinates(finish)}"]
|
|
132
130
|
end
|
|
133
131
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# Segment arithmetic for connector bridges: where two segments cross at
|
|
7
|
+
# any slope, and where the arc chord sits on the carrier segment.
|
|
8
|
+
module SegmentGeometry
|
|
9
|
+
EPSILON = 1e-9
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def distance(point, other)
|
|
14
|
+
Math.hypot(point[0] - other[0], point[1] - other[1])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 0 for a horizontal segment, 1 for a vertical one, nil otherwise.
|
|
18
|
+
def axis_for(first, last)
|
|
19
|
+
return 0 if first[1] == last[1] && first[0] != last[0]
|
|
20
|
+
return 1 if first[0] == last[0] && first[1] != last[1]
|
|
21
|
+
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A proper crossing of two segments; parallel and collinear segments
|
|
26
|
+
# never cross. Each span must leave `radius` of room either side.
|
|
27
|
+
def crossing(first, last, start, finish, radius)
|
|
28
|
+
point = axis_crossing(first, last, start, finish) ||
|
|
29
|
+
slanted_crossing(first, last, start, finish)
|
|
30
|
+
return unless point && room?(point, first, last, radius) && room?(point, start, finish, radius)
|
|
31
|
+
return unless chord_fits?(first, last, point, radius)
|
|
32
|
+
|
|
33
|
+
point
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The arc chord, one radius either side of the crossing along the
|
|
37
|
+
# carrier's dominant axis. A horizontal/vertical carrier keeps the
|
|
38
|
+
# exact crossing. On a slanted carrier EA seats the centre on the
|
|
39
|
+
# integer grid at floor(crossing) and offsets it by the half-chord
|
|
40
|
+
# (radius, truncated radius * slope), signed with the line direction.
|
|
41
|
+
def chord(first, last, crossing, radius)
|
|
42
|
+
major = dominant_axis(first, last)
|
|
43
|
+
step = last[major] > first[major] ? radius : -radius
|
|
44
|
+
return axis_chord(first, crossing, major, step) if first[1 - major] == last[1 - major]
|
|
45
|
+
|
|
46
|
+
slanted_chord(first, last, crossing, major, step)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def slanted_chord(first, last, crossing, major, step)
|
|
50
|
+
centre = crossing.map { |value| (value + EPSILON).floor }
|
|
51
|
+
half = half_chord(first, last, major, step)
|
|
52
|
+
[-1, 1].map { |sign| centre.zip(half).map { |c, h| c + sign * h } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def half_chord(first, last, major, step)
|
|
56
|
+
minor = 1 - major
|
|
57
|
+
slope = (last[minor] - first[minor]).fdiv(last[major] - first[major])
|
|
58
|
+
major.zero? ? [step, (step * slope).truncate] : [(step * slope).truncate, step]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def axis_chord(first, crossing, major, step)
|
|
62
|
+
start = crossing.dup
|
|
63
|
+
finish = crossing.dup
|
|
64
|
+
start[1 - major] = finish[1 - major] = first[1 - major]
|
|
65
|
+
start[major] -= step
|
|
66
|
+
finish[major] += step
|
|
67
|
+
[start, finish]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Integer seating must not extend the decorated carrier past an end.
|
|
71
|
+
def chord_fits?(first, last, point, radius)
|
|
72
|
+
chord(first, last, point, radius).all? do |seat|
|
|
73
|
+
[0, 1].all? { |axis| within?(seat[axis], first[axis], last[axis]) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dominant_axis(first, last)
|
|
78
|
+
(last[0] - first[0]).abs >= (last[1] - first[1]).abs ? 0 : 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def axis_crossing(first, last, start, finish)
|
|
82
|
+
axis = axis_for(first, last)
|
|
83
|
+
return unless axis && axis_for(start, finish) == 1 - axis
|
|
84
|
+
|
|
85
|
+
axis.zero? ? [start[0], first[1]] : [first[0], start[1]]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def slanted_crossing(first, last, start, finish)
|
|
89
|
+
return if axis_for(first, last) && axis_for(start, finish)
|
|
90
|
+
|
|
91
|
+
direction = delta(first, last)
|
|
92
|
+
other = delta(start, finish)
|
|
93
|
+
denominator = cross(direction, other)
|
|
94
|
+
return if denominator.zero?
|
|
95
|
+
|
|
96
|
+
advance(first, direction, cross(delta(first, start), other).fdiv(denominator))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def advance(origin, direction, fraction)
|
|
100
|
+
[origin[0] + fraction * direction[0], origin[1] + fraction * direction[1]]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def delta(from, to)
|
|
104
|
+
[to[0] - from[0], to[1] - from[1]]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def cross(one, other)
|
|
108
|
+
one[0] * other[1] - one[1] * other[0]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def room?(point, first, last, radius)
|
|
112
|
+
distance(point, first) >= radius && distance(point, last) >= radius &&
|
|
113
|
+
[0, 1].all? { |axis| within?(point[axis], first[axis], last[axis]) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def within?(value, from, to)
|
|
117
|
+
minimum, maximum = [from, to].minmax
|
|
118
|
+
value >= minimum - EPSILON && value <= maximum + EPSILON
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/ea/svg/ea_emitter.rb
CHANGED
|
@@ -33,6 +33,8 @@ module Ea
|
|
|
33
33
|
autoload :Connectors, "ea/svg/ea_emitter/connectors"
|
|
34
34
|
autoload :ConnectorBridges, "ea/svg/ea_emitter/connector_bridges"
|
|
35
35
|
private_constant :ConnectorBridges
|
|
36
|
+
autoload :SegmentGeometry, "ea/svg/ea_emitter/segment_geometry"
|
|
37
|
+
private_constant :SegmentGeometry
|
|
36
38
|
autoload :Markers, "ea/svg/ea_emitter/markers"
|
|
37
39
|
autoload :Labels, "ea/svg/ea_emitter/labels"
|
|
38
40
|
autoload :GhostLabels, "ea/svg/ea_emitter/ghost_labels"
|
data/lib/ea/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.139
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -977,6 +977,7 @@ files:
|
|
|
977
977
|
- lib/ea/svg/ea_emitter/markers.rb
|
|
978
978
|
- lib/ea/svg/ea_emitter/orthogonal_ray.rb
|
|
979
979
|
- lib/ea/svg/ea_emitter/render_context.rb
|
|
980
|
+
- lib/ea/svg/ea_emitter/segment_geometry.rb
|
|
980
981
|
- lib/ea/svg/ea_emitter/style.rb
|
|
981
982
|
- lib/ea/svg/ea_emitter/text_renderer.rb
|
|
982
983
|
- lib/ea/svg/ea_emitter/visibility_symbol.rb
|