ea 0.6.105 → 0.6.106

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 430568b3607d439af39bac818b48ee27caa207875c7f2015e3a6f5689419680b
4
- data.tar.gz: dfb835e2208d50a51caa01acb96a30e2561f5ccbdf6e83a6a1c55ec368c4b5b8
3
+ metadata.gz: 944b29164fec6d17c5e89224547ae993a7ef4dfbee74ac1c15208eba6f88f0d5
4
+ data.tar.gz: e0d40f4203d9ef1bf8a1d6f0cf72349b7e3e052ed2c3c920f96f1ca655ff2741
5
5
  SHA512:
6
- metadata.gz: ce35bed8f6b13d1f799d8c99b550ad70d19453e2f2827888c1dce54b246dc9a938baa52cfe2d24f9fb9f093ecce1a86b52c4dbecbb60b397777c592cc61ea654
7
- data.tar.gz: 22747dc14774fbfaa984efba1c88078619d8ab6bc74b4d06d5276074a93957dccc071963574aad3ff272377d043e51e613723708d0fa48318acf6558ec920de1
6
+ metadata.gz: 4087c56867c690c67f9c99011a3e8bd4c043fdcb4bb25880a6d7020d7bab5c6e9bed6adc5b25475e37d9dfe64034d6ce7a52fea44d163fb7a41df37998ce1186
7
+ data.tar.gz: 4ae9f0ff3fc328d2f4ceeec2a9e8b0acb51fc72326c9806b00471449dfc656d3b60029f002b4bd5e59269c32807144ed1a660dafb29bc9b4916731e5ba57e761
@@ -10,7 +10,16 @@ module Ea
10
10
  private
11
11
 
12
12
  def restore_stored_square_ports(model, link_row, diagram_row, geom, context)
13
- return model if context || !stored_square_port_style?(model, link_row, geom)
13
+ return model if context
14
+
15
+ model = mark_orthogonal_origin_ray(model, link_row, diagram_row, geom)
16
+ return model if model.style.to_h[:orthogonal_origin_ray] == true
17
+
18
+ restore_global_square_ports(model, link_row, diagram_row, geom)
19
+ end
20
+
21
+ def restore_global_square_ports(model, link_row, diagram_row, geom)
22
+ return model unless stored_square_port_style?(model, link_row, geom)
14
23
 
15
24
  connector = database.find_connector(link_row.connectorid)
16
25
  points = stored_square_port_coordinates(connector)
@@ -25,6 +34,50 @@ module Ea
25
34
  model
26
35
  end
27
36
 
37
+ # A square route with no stored bends uses the original mathematical
38
+ # ray to choose its orthogonal dock axis at render time.
39
+ def mark_orthogonal_origin_ray(model, link_row, diagram_row, geom)
40
+ return model unless orthogonal_origin_style?(model, link_row, geom)
41
+
42
+ origins = orthogonal_math_origins(link_row, diagram_row, geom)
43
+ return model unless origins
44
+
45
+ model.source_anchor, model.target_anchor = origins
46
+ model.waypoints = origins.map { |point| Ea::Model::Waypoint.new(position: point) }
47
+ model.style = model.style.to_h.merge(orthogonal_origin_ray: true)
48
+ model
49
+ end
50
+
51
+ def orthogonal_math_origins(link_row, diagram_row, geom)
52
+ connector = database.find_connector(link_row.connectorid)
53
+ boxes = stored_square_port_boxes(connector, diagram_row)
54
+ return unless boxes && orthogonal_box_gap?(*boxes)
55
+
56
+ offsets = [[geom[:sx], geom[:sy]], [geom[:ex], geom[:ey]]]
57
+ origins = boxes.zip(offsets).map { |box, offset| stored_ray_origin(box, *offset) }
58
+ origins if orthogonal_origins_inside?(origins, boxes)
59
+ end
60
+
61
+ def orthogonal_origins_inside?(origins, boxes)
62
+ origins.zip(boxes).all? { |point, box| origin_inside_outline?(point, box) }
63
+ end
64
+
65
+ def orthogonal_origin_style?(model, link_row, geom)
66
+ %w[Association Generalization].include?(model.connector_type) && link_row.path.to_s.empty? &&
67
+ model.style.to_h.values_at(:mode, :tree, :lwidth) == %w[3 OS 2] &&
68
+ %i[sx sy ex ey].all? { |key| !geom[key].nil? }
69
+ end
70
+
71
+ def orthogonal_box_gap?(source, target)
72
+ [source, target].all? { |box| box.width.positive? && box.height.positive? } &&
73
+ (horizontal_ray_gap?(source, target) ^ vertical_ray_gap?(source, target))
74
+ end
75
+
76
+ def origin_inside_outline?(point, box)
77
+ point.x.between?(box.x - 1, box.x + box.width + 1) &&
78
+ point.y.between?(box.y - 1, box.y + box.height + 1)
79
+ end
80
+
28
81
  def stored_square_port_style?(model, link_row, geom)
29
82
  style = model.style.to_h
30
83
  model.connector_type == "Association" && link_row.path.to_s.empty? &&
@@ -34,6 +34,9 @@ module Ea
34
34
  return nil unless source_bounds && target_bounds
35
35
  return nil if source_ref == target_ref
36
36
 
37
+ square = OrthogonalRay.pairs_for(connector, source_bounds, target_bounds)
38
+ return square if square
39
+
37
40
  if lateral
38
41
  target = lateral_target(points[1], target_bounds, connector.style[:tree])
39
42
  return target && [points[0], points[1], target]
@@ -56,6 +59,7 @@ module Ea
56
59
  width: box.width + 2, height: box.height + 2)
57
60
  end
58
61
  candidate = clipped_pairs(outline, anchors)
62
+ candidate[0] = external_source_point(connector, outline.first, anchors, candidate.first)
59
63
  expanded = forward_route?(original, candidate)
60
64
  pairs = expanded ? candidate : original
61
65
  target = expanded ? outline.last : target_bounds
@@ -80,6 +84,20 @@ module Ea
80
84
  end
81
85
  private_class_method :auto_port_elbow_pairs
82
86
 
87
+ # An external saved ray can cross two source edges; retain its
88
+ # clockwise perimeter choice instead of the nearest crossing.
89
+ def external_source_point(connector, source, anchors, fallback)
90
+ style = connector.style.to_h
91
+ return fallback unless style[:stored_association_ray] == true && style[:lwidth] == "2"
92
+
93
+ first, last = anchors
94
+ return fallback if first.x.between?(source.x, source.x + source.width) &&
95
+ first.y.between?(source.y, source.y + source.height)
96
+
97
+ anchor_exit(first, source, last, perimeter: true)
98
+ end
99
+ private_class_method :external_source_point
100
+
83
101
  # EDGE=1 explicitly seats an external source ray on the top edge;
84
102
  # its first contact can instead be the bottom when its origin is below.
85
103
  def stored_top_pairs(connector, source, pairs)
@@ -290,10 +308,9 @@ module Ea
290
308
  end
291
309
  private_class_method :forward_route?
292
310
 
293
- # Exit point of the anchor-to-anchor segment through a box's
294
- # outline: smallest positive t whose crossing lies on the
295
- # rectangle perimeter.
296
- def anchor_exit(anchor, bounds, other)
311
+ # Interior rays use the nearest forward exit. External saved sources
312
+ # can request the first intersecting edge in clockwise perimeter order.
313
+ def anchor_exit(anchor, bounds, other, perimeter: false)
297
314
  ax, ay = anchor.x.to_f, anchor.y.to_f
298
315
  ox, oy = other.x.to_f, other.y.to_f
299
316
  dx, dy = ox - ax, oy - ay
@@ -314,12 +331,25 @@ module Ea
314
331
  x >= bounds.x - 0.01 && x <= bounds.x + bounds.width + 0.01 &&
315
332
  y >= bounds.y - 0.01 && y <= bounds.y + bounds.height + 0.01
316
333
  end
317
- best = on_perimeter.min_by(&:first)
334
+ best = if perimeter
335
+ on_perimeter.min_by { |_t, x, y| perimeter_order(x, y, bounds) }
336
+ else
337
+ on_perimeter.min_by(&:first)
338
+ end
318
339
  # EA floors the dock coordinates (F851A657: 297.75 -> 297,
319
340
  # 433.7 -> 433) - a rounded-up dock is a visible miss.
320
341
  best ? [best[1].floor, best[2].floor] : [ax.floor, ay.floor]
321
342
  end
322
343
 
344
+ def perimeter_order(point_x, point_y, bounds)
345
+ return 0 if point_y == bounds.y
346
+ return 1 if point_x == bounds.x + bounds.width
347
+ return 2 if point_y == bounds.y + bounds.height
348
+
349
+ 3
350
+ end
351
+ private_class_method :perimeter_order
352
+
323
353
  # EA never docks a horizontal connector line at the header
324
354
  # divider: endpoints landing on (box side edge, divider_y)
325
355
  # move down one row slot to divider_y + 13 - the first
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Orthogonal docking from the nearer physical intersection of a stored ray.
7
+ module OrthogonalRay
8
+ module_function
9
+
10
+ def pairs_for(connector, source, target)
11
+ return unless eligible?(connector, source, target)
12
+
13
+ origins = [connector.source_anchor, connector.target_anchor]
14
+
15
+ boxes = [source, target]
16
+ physical = boxes.map { |box| outline(box) }
17
+ clipped = physical_intersections(origins, physical)
18
+ selected = nearer_origin(clipped, origins)
19
+ orthogonal_pairs(clipped, selected, boxes, physical, origins) unless selected.nil?
20
+ end
21
+
22
+ def eligible?(connector, source, target)
23
+ connector.style.to_h[:orthogonal_origin_ray] == true && source && target &&
24
+ connector.source_anchor && connector.target_anchor
25
+ end
26
+ private_class_method :eligible?
27
+
28
+ def physical_intersections(origins, physical)
29
+ origins.each_with_index.map do |origin, index|
30
+ ConnectorRedock.anchor_exit(origin, physical[index], origins[1 - index])
31
+ end
32
+ end
33
+ private_class_method :physical_intersections
34
+
35
+ def outline(box)
36
+ Ea::Model::Bounds.new(x: box.x - 1, y: box.y - 1,
37
+ width: box.width + 2, height: box.height + 2)
38
+ end
39
+ private_class_method :outline
40
+
41
+ def nearer_origin(clipped, origins)
42
+ distances = clipped.zip(origins).map do |(x, y), origin|
43
+ (x - origin.x)**2 + (y - origin.y)**2
44
+ end
45
+ distances.each_with_index.min.last unless distances.first == distances.last
46
+ end
47
+ private_class_method :nearer_origin
48
+
49
+ def orthogonal_pairs(clipped, selected, boxes, physical, origins)
50
+ point = clipped[selected]
51
+ projected = project_opposite(point, physical[selected], boxes[1 - selected])
52
+ return unless projected && forward_projection?(point, projected, origins, selected)
53
+
54
+ selected.zero? ? [point, projected] : [projected, point]
55
+ end
56
+ private_class_method :orthogonal_pairs
57
+
58
+ def project_opposite(point, own, other)
59
+ case physical_edge_axis(point, own)
60
+ when :horizontal then horizontal_projection(point, other)
61
+ when :vertical then vertical_projection(point, other)
62
+ end
63
+ end
64
+ private_class_method :project_opposite
65
+
66
+ def physical_edge_axis(point, own)
67
+ horizontal = [own.y, own.y + own.height].include?(point[1])
68
+ vertical = [own.x, own.x + own.width].include?(point[0])
69
+ return unless horizontal ^ vertical
70
+
71
+ horizontal ? :horizontal : :vertical
72
+ end
73
+ private_class_method :physical_edge_axis
74
+
75
+ def horizontal_projection(point, other)
76
+ x, y = point
77
+ return unless x.between?(other.x, other.x + other.width)
78
+
79
+ edge = facing_edge(y, other.y, other.height)
80
+ [x, edge] if edge
81
+ end
82
+ private_class_method :horizontal_projection
83
+
84
+ def vertical_projection(point, other)
85
+ x, y = point
86
+ return unless y.between?(other.y, other.y + other.height)
87
+
88
+ edge = facing_edge(x, other.x, other.width)
89
+ [edge, y] if edge
90
+ end
91
+ private_class_method :vertical_projection
92
+
93
+ def facing_edge(coordinate, start, extent)
94
+ return start if coordinate < start
95
+ return start + extent if coordinate > start + extent
96
+
97
+ nil
98
+ end
99
+ private_class_method :facing_edge
100
+
101
+ def forward_projection?(point, projected, origins, selected)
102
+ first, last = origins.values_at(selected, 1 - selected)
103
+ x, y = point
104
+ target_x, target_y = projected
105
+ ((last.x - first.x) * (target_x - x) + (last.y - first.y) * (target_y - y)).positive?
106
+ end
107
+ private_class_method :forward_projection?
108
+ end
109
+ end
110
+ end
111
+ end
@@ -45,6 +45,9 @@ module Ea
45
45
  autoload :Document, "ea/svg/ea_emitter/document"
46
46
  autoload :Label, "ea/svg/ea_emitter/label"
47
47
  autoload :Compartment, "ea/svg/ea_emitter/compartment"
48
+ autoload :OrthogonalRay, "ea/svg/ea_emitter/orthogonal_ray"
49
+ private_constant :OrthogonalRay
50
+
48
51
  autoload :ConnectorRedock, "ea/svg/ea_emitter/connector_redock"
49
52
  autoload :RenderContext, "ea/svg/ea_emitter/render_context"
50
53
  autoload :VisibilitySymbol, "ea/svg/ea_emitter/visibility_symbol"
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.105"
7
+ VERSION = "0.6.106"
8
8
  end
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.105
4
+ version: 0.6.106
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-15 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -950,6 +950,7 @@ files:
950
950
  - lib/ea/svg/ea_emitter/marker/plus.rb
951
951
  - lib/ea/svg/ea_emitter/marker/registry.rb
952
952
  - lib/ea/svg/ea_emitter/markers.rb
953
+ - lib/ea/svg/ea_emitter/orthogonal_ray.rb
953
954
  - lib/ea/svg/ea_emitter/render_context.rb
954
955
  - lib/ea/svg/ea_emitter/style.rb
955
956
  - lib/ea/svg/ea_emitter/text_renderer.rb