ea 0.6.100 → 0.6.101

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: 8f60cf58bb521e911eb26fb30b628162e149a9521027fae0e77756ef1b8354dd
4
- data.tar.gz: c0580ced4122e41650fbc5053fb0fb534a40c8cae84f758039dd8b2f575422ad
3
+ metadata.gz: 7c722ef7c5b7c7968f5beb08538b31b8090a3ab1533b0418a9600752c2724ef3
4
+ data.tar.gz: 818694e4771383a00ae453f76a7b3f98e79b6b5a839d9459d415ec72129e4226
5
5
  SHA512:
6
- metadata.gz: 6460bd84357b97c350c485526bcc62bc74309d924eacd9e7983347b101c22d1fcfc246dc7b76d443eb04cb360e2be890eaf1cffedf49103e9f2419b5205fe3a7
7
- data.tar.gz: 2a8ce3556ffa2b5531e432002ab7a67439cd4f4ed171c1f84f141b264e9fa6913af3ffd8983dbb0be70c9282407fdb3695efb07728c0217c656688ed38f8d023
6
+ metadata.gz: b00b8fddbd7701f724dc20bd6640dfc53b7a99903d7a075581be94c9f3674254a114fa976a2a895d5ba231ca3b0c93f7b8ce241c42ef4d5c9e106f82c7b660a6
7
+ data.tar.gz: c41701279192e06adba33b39063dac26a9355dedf1a46fd687079013c341c880f8f3d83278876593cf27733c527f41e22032140b901246b0b6e335cf29dcb308
@@ -113,21 +113,38 @@ module Ea
113
113
  return model unless association_ray_candidate?(model, link_row)
114
114
 
115
115
  source, target = stored_ray_boxes(link_row, diagram_row)
116
- origins = association_ray_origins(source, target, geom)
116
+ origins = association_ray_origins(source, target, geom, model.style.to_h[:lwidth])
117
117
  return model unless origins
118
118
 
119
- model.waypoints = [model.source_port, model.target_port].map do |point|
120
- Ea::Model::Waypoint.new(position: point)
121
- end
119
+ model.waypoints = port_waypoints(model)
122
120
  model.source_anchor, model.target_anchor = origins
121
+ model.style = model.style.to_h.merge(stored_association_ray: true, association_ray_outline: true)
123
122
  model
124
123
  end
125
124
 
126
- def association_ray_origins(source, target, geom)
125
+ def port_waypoints(model)
126
+ [model.source_port, model.target_port].map do |point|
127
+ Ea::Model::Waypoint.new(position: point)
128
+ end
129
+ end
130
+
131
+ # Width-2 boxes also own their 1px outer stroke, so a stored
132
+ # origin on that outline is still inside its box.
133
+ def association_ray_origins(source, target, geom, width)
134
+ outline = width == "2" ? 1 : 0
127
135
  origins = [stored_ray_origin(source, geom[:sx], geom[:sy]),
128
136
  stored_ray_origin(target, geom[:ex], geom[:ey])]
129
- inside = origins.zip([source, target]).all? { |o, b| inside_box?(o, b) }
130
- origins if inside && sub_pixel_safe_origins?(origins, [source, target])
137
+ boxes = [source, target].map { |box| grown_bounds(box, outline) }
138
+ inside = origins.zip(boxes).all? { |o, b| inside_box?(o, b) }
139
+ return unless inside && origins.first != origins.last
140
+
141
+ origins if width == "2" || sub_pixel_safe_origins?(origins, [source, target] + boxes)
142
+ end
143
+
144
+ def grown_bounds(bounds, amount)
145
+ Ea::Model::Bounds.new(x: bounds.x - amount, y: bounds.y - amount,
146
+ width: bounds.width + 2 * amount,
147
+ height: bounds.height + 2 * amount)
131
148
  end
132
149
 
133
150
  def association_ray_candidate?(model, link_row)
@@ -141,15 +158,13 @@ module Ea
141
158
  %i[direct regenerated].none? { |key| style.key?(key) }
142
159
  end
143
160
 
144
- # Origins are whole pixels while EA's box centers may sit on a
145
- # half pixel. An origin on its border then clips differently
161
+ # Width-0 origins are whole pixels while EA's box centers may sit
162
+ # on a half pixel. An origin on its border then clips differently
146
163
  # unless the line is axis-aligned, which no rounding changes.
147
164
  def sub_pixel_safe_origins?(origins, boxes)
148
165
  first, last = origins
149
- return false if first == last
150
-
151
166
  axis_aligned = first.x == last.x || first.y == last.y
152
- axis_aligned || origins.zip(boxes).none? { |point, box| on_border?(point, box) }
167
+ axis_aligned || origins.none? { |point| boxes.any? { |box| on_border?(point, box) } }
153
168
  end
154
169
 
155
170
  def on_border?(point, bounds)
@@ -352,6 +367,14 @@ module Ea
352
367
  route
353
368
  end
354
369
 
370
+ # One stored corner that is level with both boxes already is the
371
+ # route (vertical leg out of the source, horizontal leg into the
372
+ # target); flattening it to the facing edges would lose it.
373
+ def level_corner_route?(points, link_row, connector, src, tgt)
374
+ points.size == 3 && level_entry_style?(link_row, connector) &&
375
+ !level_entry_point(points[1], src).nil? && !level_entry_point(points[1], tgt).nil?
376
+ end
377
+
355
378
  def level_entry_style?(link_row, connector)
356
379
  style = DiagramStyleParser.parse(link_row.style)
357
380
  %w[Generalization Aggregation].include?(connector.connector_type) &&
@@ -419,6 +442,62 @@ module Ea
419
442
  [bounds.y, bounds.y + bounds.height].include?(point[1])
420
443
  end
421
444
 
445
+ # A width-2 Mode 3 Aggregation without a tree style keeps an authored
446
+ # orthogonal stair-step of three or more corners. Each end docks on
447
+ # the 1px outline its adjacent leg points at, from the corner's own
448
+ # coordinate; any end that cannot do so leaves the route unchanged.
449
+ def stored_stair_route(link_row, connector, source, target)
450
+ return nil unless stair_style?(link_row, connector)
451
+
452
+ corners = stored_square_corners(link_row)
453
+ return nil unless corners && corners.size >= 3 && alternating_legs?(corners)
454
+
455
+ first = outline_dock(corners[0], corners[1], source)
456
+ last = outline_dock(corners[-1], corners[-2], target)
457
+ [first, *corners, last] if first && last
458
+ end
459
+
460
+ def stair_style?(link_row, connector)
461
+ style = DiagramStyleParser.parse(link_row.style)
462
+ connector.connector_type == "Aggregation" && style[:mode] == "3" &&
463
+ style[:lwidth] == "2" && !style.key?(:tree)
464
+ end
465
+
466
+ # Every leg is non-zero and runs along the opposite axis of the last.
467
+ def alternating_legs?(corners)
468
+ axes = corners.each_cons(2).map { |from, to| leg_axis(from, to) }
469
+ axes.none?(&:nil?) && axes.each_cons(2).all? { |a, b| a != b }
470
+ end
471
+
472
+ def leg_axis(from, to)
473
+ return :vertical if from[0] == to[0] && from[1] != to[1]
474
+
475
+ :horizontal if from[1] == to[1] && from[0] != to[0]
476
+ end
477
+
478
+ # The dock runs perpendicular to the corner's own leg.
479
+ def outline_dock(corner, neighbour, box)
480
+ if corner[1] == neighbour[1]
481
+ vertical_outline_dock(corner, box)
482
+ else
483
+ horizontal_outline_dock(corner, box)
484
+ end
485
+ end
486
+
487
+ def vertical_outline_dock(corner, box)
488
+ x, y = corner
489
+ return nil unless x.between?(box.x, box.x + box.width) && !within_height?(y, box)
490
+
491
+ [x, y < box.y ? box.y - 1 : box.y + box.height + 1]
492
+ end
493
+
494
+ def horizontal_outline_dock(corner, box)
495
+ x, y = corner
496
+ return nil unless within_height?(y, box) && !x.between?(box.x, box.x + box.width)
497
+
498
+ [x < box.x ? box.x - 1 : box.x + box.width + 1, y]
499
+ end
500
+
422
501
  # A valid lateral corner joins one vertical leg and one horizontal
423
502
  # leg. Keep it even when the boxes also admit a shorter direct route.
424
503
  # ported: both ends carry explicit SX/SY and EX/EY offsets. An
@@ -433,6 +512,66 @@ module Ea
433
512
  corner && lateral_corner_route(style, corner, source, target)
434
513
  end
435
514
 
515
+ # A horizontal lateral tree with no usable stored corner leaves
516
+ # the source's top or bottom edge at its center x, turns at the
517
+ # target's center y and enters the target's facing side edge.
518
+ # When that y lies inside the source's height the route is one
519
+ # horizontal line between the facing side edges. Every dock is
520
+ # flush on its box edge.
521
+ def derived_lateral_route(link_row, connector, source, target)
522
+ style = DiagramStyleParser.parse(link_row.style)
523
+ return nil unless style[:mode] == "3" && style[:tree] == "LH" &&
524
+ derived_lateral_path?(connector.connector_type, link_row.path.to_s)
525
+
526
+ x = source.x + source.width / 2
527
+ y = target.y + target.height / 2
528
+ edge = lateral_target_edge(x, target)
529
+ return nil unless edge
530
+
531
+ if within_height?(y, source)
532
+ side = target.x > x ? source.x + source.width : source.x
533
+ [[side, y], [edge, y]] if ((edge - side) * (edge - x)).positive?
534
+ else
535
+ [[x, y < source.y ? source.y : source.y + source.height], [x, y], [edge, y]]
536
+ end
537
+ end
538
+
539
+ # An Association may carry one stored corner that no valid lateral
540
+ # route accepted; a Generalization derives only from an empty Path.
541
+ def derived_lateral_path?(connector_type, path)
542
+ case connector_type
543
+ when "Association" then path.match?(/\A(?:-?\d+:-?\d+;?)?\z/)
544
+ when "Generalization" then path.empty?
545
+ else false
546
+ end
547
+ end
548
+
549
+ # A derived level line docks flush at its own two ends; anchoring
550
+ # the ray there keeps the emitter's clip from moving them.
551
+ def pin_derived_lateral_line(model, link_row, diagram_row, context)
552
+ style = model.style.to_h
553
+ return model if context || model.waypoints.size != 2 ||
554
+ style[:mode] != "3" || style[:tree] != "LH"
555
+
556
+ source, target = stored_ray_boxes(link_row, diagram_row)
557
+ connector = database.find_connector(link_row.connectorid)
558
+ line = connector && derived_lateral_route(link_row, connector, source, target)
559
+ return model unless line && line.size == 2 &&
560
+ line == model.waypoints.map { |wp| [wp.position.x, wp.position.y] }
561
+
562
+ model.source_anchor, model.target_anchor = line.map { |x, y| Ea::Model::Point.new(x: x, y: y) }
563
+ model.style = style.merge(derived_lateral_line: true)
564
+ model
565
+ end
566
+
567
+ # The target side edge a vertical leg at x faces, or nil when x
568
+ # lies within the target's span.
569
+ def lateral_target_edge(x_position, target)
570
+ return target.x if x_position < target.x
571
+
572
+ target.x + target.width if x_position > target.x + target.width
573
+ end
574
+
436
575
  # An untyped Mode 3 Association corner takes whichever leg
437
576
  # orientation its position fits; width-2 docks sit on the 1px
438
577
  # outer outline.
@@ -613,12 +752,44 @@ module Ea
613
752
  model
614
753
  end
615
754
 
755
+ # A two-waypoint Mode 3 Aggregation with only a target port draws the
756
+ # line from the source center to the target's stored ray origin
757
+ # (center + EX, center - EY), clipped at the outlines; the boundary
758
+ # port is only the derived crossing of that origin's ray.
759
+ def correct_partial_port_origins(model, link_row, diagram_row, geom, context)
760
+ return model if context || !partial_port_origin_candidate?(model, link_row)
761
+
762
+ source, target = stored_ray_boxes(link_row, diagram_row)
763
+ return model unless horizontal_ray_gap?(source, target)
764
+
765
+ model.source_anchor = stored_ray_origin(source, 0, 0)
766
+ model.target_anchor = stored_ray_origin(target, geom[:ex], geom[:ey])
767
+ model
768
+ end
769
+
770
+ def partial_port_origin_candidate?(model, link_row)
771
+ style = model.style.to_h
772
+ model.connector_type == "Aggregation" && model.waypoints.size == 2 &&
773
+ link_row.path.to_s.empty? && style[:mode] == "3" && style[:lwidth] == "2" &&
774
+ !style.key?(:tree) &&
775
+ model.source_port.nil? && model.target_port
776
+ end
777
+
616
778
  def partial_port_jog_candidate?(model, link_row)
617
779
  style = model.style.to_h
618
780
  model.connector_type == "Aggregation" &&
619
- link_row.path.to_s.empty? && style[:mode] == "3" && style[:lwidth] == "2" &&
620
- !style.key?(:tree) && model.source_port.nil? && model.target_port &&
621
- model.waypoints.size == 2
781
+ link_row.path.to_s.empty? && style[:mode] == "3" &&
782
+ !style.key?(:tree) && model.waypoints.size == 2 &&
783
+ explicit_port_side?(model, style[:lwidth])
784
+ end
785
+
786
+ # Width 2 keeps the evidenced target-only port; width 0 accepts a
787
+ # single explicit port at either end.
788
+ def explicit_port_side?(model, width)
789
+ ports = [model.source_port, model.target_port]
790
+ return model.source_port.nil? && !model.target_port.nil? if width == "2"
791
+
792
+ width == "0" && ports.compact.size == 1
622
793
  end
623
794
 
624
795
  def computed_jog_candidate?(model, link_row, geom)
@@ -362,12 +362,20 @@ module Ea
362
362
  target_stereotype: conn.deststereotype,
363
363
  waypoints: waypoints,
364
364
  label_boxes: DEFAULT_LABEL_BOXES,
365
- style: PHANTOM_DOUBLE_BACK_TYPES.include?(conn.connector_type) ? { direct: true } : {},
365
+ style: phantom_style(conn.connector_type),
366
366
  hidden: false,
367
367
  has_geometry_offsets: false
368
368
  )
369
369
  end
370
370
 
371
+ # Internal producer flag, like :direct. A model relationship with no
372
+ # link row is stroked at the 2px box outline, so its ends dock there.
373
+ def phantom_style(connector_type)
374
+ return { direct: true } if PHANTOM_DOUBLE_BACK_TYPES.include?(connector_type)
375
+
376
+ %w[Aggregation Association].include?(connector_type) ? { phantom_outline: true } : {}
377
+ end
378
+
371
379
  # Computes the edge attachment point on source bounds that
372
380
  # faces the target bounds. Uses the center-to-center direction
373
381
  # to pick the nearest edge intersection.
@@ -422,7 +430,9 @@ module Ea
422
430
  is_substitutable: substitutable_connector?(connector)
423
431
  ), link_row, diagram_row, geom)
424
432
  model = collapse_partial_port_jog(model, link_row, diagram_row)
433
+ model = correct_partial_port_origins(model, link_row, diagram_row, geom, context)
425
434
  model = direct_empty_path_ray(model, link_row, diagram_row, geom, context)
435
+ model = pin_derived_lateral_line(model, link_row, diagram_row, context)
426
436
  model = mode2_overlap_ray(model, link_row, diagram_row, geom, context)
427
437
  model = association_stored_ray(model, link_row, diagram_row, geom, context)
428
438
  model = correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
@@ -666,15 +676,17 @@ module Ea
666
676
  src = bounds_from_rect(source_placement)
667
677
  tgt = bounds_from_rect(target_placement)
668
678
  tree_route = stored_square_route(link_row, connector, src, tgt) ||
679
+ stored_stair_route(link_row, connector, src, tgt) ||
669
680
  stored_lateral_route(link_row, connector, src, tgt,
670
681
  ported: source_port && target_port) ||
682
+ derived_lateral_route(link_row, connector, src, tgt) ||
671
683
  horizontal_tree_route(points, link_row, connector, src, tgt)
672
684
  if tree_route
673
685
  points = tree_route
674
686
  elsif %w[Association Aggregation].include?(connector.connector_type) &&
675
687
  edge_out != 2
676
688
  stored_tree = preserve_stored_tree?(points, link_row, connector, src, tgt)
677
- unless stored_tree
689
+ unless stored_tree || level_corner_route?(points, link_row, connector, src, tgt)
678
690
  points = facing_edge_route(points, src, tgt)
679
691
  end
680
692
  # A width-2 Aggregation that keeps its stored bends is
@@ -39,10 +39,15 @@ module Ea
39
39
  return target && [points[0], points[1], target]
40
40
  end
41
41
 
42
+ return nil if flush_lateral_line?(connector, points, source_bounds, target_bounds)
43
+
42
44
  anchors = [connector.source_anchor, connector.target_anchor]
43
45
  bounds = [source_bounds, target_bounds]
44
46
  original = clipped_pairs(bounds, anchors)
45
- return original unless stroke_outline?(connector, bounds) || overlap_ray?(connector)
47
+ unless stroke_outline?(connector, bounds) || overlap_ray?(connector) ||
48
+ bordered_outline?(connector, diagram, [source_ref, target_ref], bounds)
49
+ return original
50
+ end
46
51
 
47
52
  # EA docks these markers on the outer edge of the 2px box stroke.
48
53
  outline = bounds.map do |box|
@@ -53,6 +58,29 @@ module Ea
53
58
  forward_route?(original, candidate) ? candidate : original
54
59
  end
55
60
 
61
+ # A horizontal lateral tree the QEA builder derived as one level
62
+ # line (internal style flag) keeps it between the facing side edges.
63
+ def flush_lateral_line?(connector, points, source, target)
64
+ style = connector.style.to_h
65
+ return false unless style[:derived_lateral_line] && style[:mode] == "3" &&
66
+ style[:tree] == "LH" && points.size == 2 &&
67
+ %w[Association Generalization].include?(connector.connector_type)
68
+ return false unless connector.source_anchor && connector.target_anchor
69
+
70
+ first, last = points
71
+ pinned = [[connector.source_anchor.x, connector.source_anchor.y],
72
+ [connector.target_anchor.x, connector.target_anchor.y]]
73
+ pinned == [first, last] && first[1] == last[1] &&
74
+ on_side_edge?(first, source) && on_side_edge?(last, target)
75
+ end
76
+ private_class_method :flush_lateral_line?
77
+
78
+ def on_side_edge?(point, bounds)
79
+ [bounds.x, bounds.x + bounds.width].include?(point[0]) &&
80
+ point[1].between?(bounds.y, bounds.y + bounds.height)
81
+ end
82
+ private_class_method :on_side_edge?
83
+
56
84
  def lateral_tree?(connector, points)
57
85
  style = connector.style.to_h
58
86
  points.size == 3 && style[:mode] == "3" && style[:lwidth] == "2" &&
@@ -97,7 +125,7 @@ module Ea
97
125
  private_class_method :facing_coordinate
98
126
 
99
127
  def stroke_outline?(connector, bounds)
100
- connector.style.to_h[:lwidth] == "2" &&
128
+ (connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector)) &&
101
129
  bounds.all? { |box| box.width.positive? && box.height.positive? } &&
102
130
  outline_connector?(connector, bounds)
103
131
  end
@@ -111,9 +139,42 @@ module Ea
111
139
  end
112
140
  private_class_method :overlap_ray?
113
141
 
142
+ # The QEA builder flags a model relationship drawn without any
143
+ # diagram link row; EA strokes it at the 2px box outline.
144
+ def default_model_connector?(connector)
145
+ connector.style.to_h[:phantom_outline] == true && !connector.has_geometry_offsets &&
146
+ %w[Aggregation Association].include?(connector.connector_type)
147
+ end
148
+ private_class_method :default_model_connector?
149
+
150
+ # A thin straight Generalization/Aggregation/Association between two elements
151
+ # drawn with a 2px border stops on the outer edge of those borders.
152
+ def bordered_outline?(connector, diagram, refs, bounds)
153
+ style = connector.style.to_h
154
+ return false unless style[:lwidth] == "0" && !style[:tree]
155
+ return false unless %w[Generalization Aggregation Association].include?(connector.connector_type)
156
+ return false unless bounds.all? { |box| box.width.positive? && box.height.positive? }
157
+
158
+ ends = [[refs.first, style[:soid]], [refs.last, style[:eoid]]]
159
+ ends.all? { |ref, duid| border_two?(diagram, ref, duid) }
160
+ end
161
+ private_class_method :bordered_outline?
162
+
163
+ # Every placement of the element (the DUID-matched one when the
164
+ # connector names it) must declare a border width of 2.
165
+ def border_two?(diagram, ref, duid)
166
+ placed = Array(diagram&.elements).select { |el| el.model_element_ref == ref }
167
+ named = placed.select { |el| duid && el.duid.to_s.casecmp?(duid.to_s) }
168
+ placed = named unless named.empty?
169
+ placed.any? && placed.all? { |el| el.line_width == 2 || el.style.to_h[:lwth] == "2" }
170
+ end
171
+ private_class_method :border_two?
172
+
114
173
  def outline_connector?(connector, bounds)
174
+ return true if default_model_connector?(connector)
115
175
  return true if %w[Generalization Aggregation].include?(connector.connector_type)
116
176
 
177
+ return true if association_ray_outline?(connector)
117
178
  return false unless stored_association?(connector) || offset_free_association?(connector)
118
179
 
119
180
  # Diagonal Association rounding and tree docks keep their existing rules.
@@ -122,6 +183,15 @@ module Ea
122
183
  end
123
184
  private_class_method :outline_connector?
124
185
 
186
+ # The QEA builder marks the stored Association rays it produced;
187
+ # their diagonal docks sit on the outer stroke edge like the
188
+ # axis-aligned ones. Unmarked diagonals keep the legacy docking.
189
+ def association_ray_outline?(connector)
190
+ connector.connector_type == "Association" &&
191
+ connector.style.to_h[:association_ray_outline] == true
192
+ end
193
+ private_class_method :association_ray_outline?
194
+
125
195
  def stored_association?(connector)
126
196
  style = connector.style.to_h
127
197
  connector.connector_type == "Association" && connector.has_geometry_offsets &&
@@ -244,12 +314,20 @@ module Ea
244
314
  # anchor row is one explicit horizontal line: EA seats neither end
245
315
  # in a compartment row, unlike Generalization/Aggregation targets.
246
316
  def explicit_association_ray?(pairs, connector)
317
+ produced = connector.style.to_h[:stored_association_ray] == true
318
+ return true if produced && pairs.first[1] == pairs.last[1]
319
+
320
+ certified_association_ray?(pairs, connector)
321
+ end
322
+
323
+ def certified_association_ray?(pairs, connector)
247
324
  return false unless explicit_horizontal_ray?(connector, %w[Association])
248
325
 
249
326
  connector.source_anchor.y == connector.target_anchor.y &&
250
327
  connector.source_port.y == connector.source_anchor.y &&
251
328
  pairs.first[1] == pairs.last[1]
252
329
  end
330
+ private_class_method :certified_association_ray?
253
331
  private_class_method :explicit_association_ray?
254
332
 
255
333
  def explicit_horizontal_ray?(connector, types = %w[Generalization Aggregation])
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.100"
7
+ VERSION = "0.6.101"
8
8
  end
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.100
4
+ version: 0.6.101
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.