ea 0.6.120 → 0.6.121

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.
@@ -51,6 +51,9 @@ module Ea
51
51
  # Advance used for codepoints missing from the tables.
52
52
  DEFAULT_ADVANCE = 0.25
53
53
 
54
+ # Inferred subpixel steps that reproduce EA's near-half-pixel rounding.
55
+ GDI_SUBPIXELS = 64
56
+
54
57
  # Carlito/Arial have no CJK glyphs; EA's renderer falls back to
55
58
  # a proportional CJK font whose advances measure ~0.709em at
56
59
  # the NOMINAL size (not scaled by the per-style factors) —
@@ -89,6 +92,13 @@ module Ea
89
92
  (size.to_f * 10 / 7).round
90
93
  end
91
94
 
95
+ # Quantize before rounding: near-half-pixel Carlito advances
96
+ # round up in EA's output. The 1/64px step is inferred.
97
+ def gdi_pixel_advance(advance, height)
98
+ ((advance * height * GDI_SUBPIXELS).round / GDI_SUBPIXELS.to_f).round
99
+ end
100
+ private_class_method :gdi_pixel_advance
101
+
92
102
  def compact_compartment_font?(size, family:, unit: "pt")
93
103
  family.to_s.casecmp("Cambria").zero? && size == 7 && unit == "pt"
94
104
  end
@@ -122,7 +132,7 @@ module Ea
122
132
  if cjk?(ch.ord)
123
133
  cjk_count += 1
124
134
  else
125
- latin += ((table[cp_key(ch)] || DEFAULT_ADVANCE) * height).round
135
+ latin += gdi_pixel_advance(table[cp_key(ch)] || DEFAULT_ADVANCE, height)
126
136
  end
127
137
  end
128
138
  return latin + cjk_count * CJK_ADVANCE * size_pt.to_f
@@ -166,7 +176,7 @@ module Ea
166
176
 
167
177
  # Families whose EA driver quantizes per-glyph advances
168
178
  # (fitted from published textLengths).
169
- GDI_HEIGHT_FAMILIES = %w[Carlito].freeze
179
+ GDI_HEIGHT_FAMILIES = %w[Carlito Arial].freeze
170
180
 
171
181
  def supported_family?(family)
172
182
  !resolve_family(family).nil?
@@ -150,14 +150,14 @@ module Ea
150
150
  )
151
151
  end
152
152
 
153
- # Negative widths inherit the saved object border, then the diagram theme.
153
+ # Negative widths inherit; QEA zero widths request a one-pixel cosmetic pen.
154
154
  def effective_line_width(obj_row, style)
155
155
  object = database.find_object(obj_row.ea_object_id)
156
156
  [style[:lwth], object&.borderwidth].each do |raw|
157
157
  next if raw.nil? || raw.to_s.empty?
158
158
 
159
159
  width = raw.to_i
160
- return width if width >= 0
160
+ return [width, 1].max if width >= 0
161
161
  end
162
162
  nil
163
163
  end
@@ -6,7 +6,7 @@ module Ea
6
6
  # Single source of truth for fill + stroke color decisions.
7
7
  #
8
8
  # Theme :119 applies:
9
- # - Border color (#9A8484) for element strokes
9
+ # - Black default element strokes
10
10
  # - Method color for operation text
11
11
  # Theme :119 does NOT apply:
12
12
  # - Per-type fill colors (elements use default #FFFFFF)
@@ -21,13 +21,8 @@ module Ea
21
21
  %(<g style="#{group_style(context)}">\n#{text_blocks.join("\n")}\n</g>)
22
22
  end
23
23
 
24
- # EA wraps note text by MEASURED width (Carlito advances),
25
- # not character count: lines break at the last word whose
26
- # cumulative integer textLength fits bounds.width - 15.
27
- # Verified against EA-published reference SVGs.
28
24
  def wrapped_lines(context, body)
29
- # EA's note wrap padding is 4: lines fit bounds.width - 4
30
- # (9BA1CE54: width 214, longest line tl 210). Pitch and
25
+ # Lines fit bounds.width - 4. Pitch and
31
26
  # first-line offset scale with the note font (9pt: pitch
32
27
  # 16 = size+7, first line at bounds.y + 18 = size+9), and
33
28
  # each paragraph break adds one blank pitch row
@@ -41,7 +36,8 @@ module Ea
41
36
  y_index = 0
42
37
  paragraphs = NoteHtml.paragraphs(body)
43
38
  paragraphs.flat_map do |para|
44
- lines = wrap_paragraph(para.text, usable, size, context.family)
39
+ lines = wrap_paragraph(para.text, usable, size, context.family,
40
+ logical_height: context.logical_height)
45
41
  paragraph_svg = lines.each_with_index.map do |line, i|
46
42
  y = context.bounds.y + first_offset + ((y_index + i) * pitch)
47
43
  TextRenderer.new(
@@ -50,6 +46,7 @@ module Ea
50
46
  y: y,
51
47
  family: context.family, size: context.size,
52
48
  size_unit: context.size_unit,
49
+ logical_height: context.logical_height,
53
50
  fill: context.theme.text_color,
54
51
  stroke_in_text: context.theme.stroke_in_text_color,
55
52
  width_factor: context.theme.text_width_factor
@@ -60,14 +57,18 @@ module Ea
60
57
  end
61
58
  end
62
59
 
63
- def wrap_paragraph(para, usable, size, family)
64
- words = para.strip.split(/\s+/)
60
+ # Wrap at whitespace until native script-specific break metrics are
61
+ # established. Every word but the paragraph's last carries its
62
+ # trailing space into the fit test.
63
+ def wrap_paragraph(para, usable, size, family, logical_height: nil)
65
64
  lines = []
66
65
  current = ""
67
- words.each do |word|
66
+ words = para.strip.split(/\s+/)
67
+ words.each_with_index do |word, index|
68
68
  candidate = current.empty? ? word : "#{current} #{word}"
69
- width = TextRenderer.estimate_width(candidate, size, nil,
70
- family: family).round
69
+ trailing = index < words.size - 1 ? " " : ""
70
+ width = TextRenderer.estimate_width("#{candidate}#{trailing}", size, nil,
71
+ family: family, logical_height: logical_height).round
71
72
  if current.empty? || width <= usable
72
73
  current = candidate
73
74
  else
@@ -233,7 +233,8 @@ module Ea
233
233
  private_class_method :facing_coordinate
234
234
 
235
235
  def stroke_outline?(connector, bounds)
236
- (connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector)) &&
236
+ (connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector) ||
237
+ connector.connector_type == "NoteLink") &&
237
238
  bounds.all? { |box| box.width.positive? && box.height.positive? } &&
238
239
  outline_connector?(connector, bounds)
239
240
  end
@@ -311,7 +312,7 @@ module Ea
311
312
 
312
313
  def outline_connector?(connector, bounds)
313
314
  return true if default_model_connector?(connector)
314
- return true if %w[Generalization Aggregation].include?(connector.connector_type)
315
+ return true if %w[Generalization Aggregation NoteLink].include?(connector.connector_type)
315
316
 
316
317
  return true if association_ray_outline?(connector)
317
318
  return false unless stored_association?(connector) || offset_free_association?(connector)
@@ -31,22 +31,39 @@ module Ea
31
31
  @divider_y_by_ref = divider_y_by_ref
32
32
  end
33
33
 
34
+ # EA draws each connector (and its markers) with the pen width
35
+ # stored in the diagram link style (`LWidth`); links without
36
+ # the field keep the diagram default.
37
+ def self.stroke_width_for(connector, default)
38
+ style = connector.style || {}
39
+ raw = style[:lwidth] || style["lwidth"]
40
+ return default if raw.to_s.strip.empty?
41
+
42
+ [raw.to_i, 1].max
43
+ end
44
+
45
+ # Layer key for a style group: the plain key at the default
46
+ # width, suffixed with the width otherwise so layers of
47
+ # different widths never merge.
48
+ def self.layer_key(key, width, default)
49
+ width == default ? key : :"#{key}_w#{width}"
50
+ end
51
+
34
52
  def layers
35
- paths = visible_connectors.filter_map { |c| path_for(c) }
36
- return [] if paths.empty?
53
+ widths = paths_with_widths
37
54
 
38
55
  if grouped
39
- [Layer.new(style_key: LINE_STYLE_KEY, style: line_style,
40
- body: paths.join("\n "))]
41
- else
42
- paths.map do |path|
43
- Layer.new(style_key: LINE_STYLE_KEY, style: line_style, body: path)
56
+ widths.group_by(&:first).map do |width, entries|
57
+ line_layer(width, entries.map(&:last).join("\n "))
44
58
  end
59
+ else
60
+ widths.map { |width, path| line_layer(width, path) }
45
61
  end
46
62
  end
47
63
 
48
- def line_style
49
- "stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:0.00; stroke:#000000; stroke-opacity:1.00"
64
+ def line_style(width = stroke_width)
65
+ "stroke-width:#{width};stroke-linecap:round;stroke-linejoin:bevel; " \
66
+ "fill:#000000;fill-opacity:0.00; stroke:#000000; stroke-opacity:1.00"
50
67
  end
51
68
 
52
69
  # Backwards-compatible render — joins all layers' `<g>`.
@@ -56,6 +73,18 @@ module Ea
56
73
 
57
74
  private
58
75
 
76
+ def paths_with_widths
77
+ visible_connectors.filter_map do |connector|
78
+ path = path_for(connector)
79
+ [self.class.stroke_width_for(connector, stroke_width), path] if path
80
+ end
81
+ end
82
+
83
+ def line_layer(width, body)
84
+ Layer.new(style_key: self.class.layer_key(LINE_STYLE_KEY, width, stroke_width),
85
+ style: line_style(width), body: body)
86
+ end
87
+
59
88
  def visible_connectors
60
89
  (diagram.connectors || []).select(&:renderable?)
61
90
  end
@@ -9,32 +9,15 @@ module Ea
9
9
  # chevron) when a stereotype provides a custom icon via
10
10
  # ShapeScript.
11
11
  #
12
- # Currently provides hardcoded fallback polygons for the most
13
- # common GML stereotypes (FeatureType, Type) since the source
14
- # ShapeScript definitions live in EA's encrypted
15
- # InternalTechnologies (TODO.diagrams/87). Future enhancement:
16
- # read shapescript from MDG files when available.
12
+ # Only MDG-provided ShapeScript is rendered.
17
13
  class StereotypeIconRenderer
18
- # Hardcoded fallback icons keyed by stereotype name.
19
- # Each entry is [fill, stroke, points_array].
20
- FALLBACK_ICONS = {
21
- "FeatureType" => ["#FAF1EC", "#69738C",
22
- [[0, -5], [5, 0], [0, 5], [-5, 0]]],
23
- "Type" => ["#FAF1EC", "#69738C",
24
- [[0, -5], [5, 0], [0, 5], [-5, 0]]]
25
- }.freeze
26
-
27
- # Default position offset from element's center
28
- OFFSET_X = 0
29
- OFFSET_Y = 0
30
-
31
14
  attr_reader :classifier, :bounds, :canvas, :mdg_registry
32
15
 
33
16
  # @param mdg_registry [Ea::Mdg::Registry, nil] optional
34
17
  # registry to look up MDG-provided ShapeScript for the
35
18
  # classifier's stereotype. When provided and the matching
36
19
  # stereotype has a ShapeScript body, it's parsed and
37
- # rendered. Otherwise falls back to FALLBACK_ICONS.
20
+ # rendered.
38
21
  def initialize(classifier:, bounds:, canvas: nil, mdg_registry: nil)
39
22
  @classifier = classifier
40
23
  @bounds = bounds
@@ -51,12 +34,12 @@ module Ea
51
34
  return "" unless classifier && bounds
52
35
  return "" unless stereotype_name
53
36
 
54
- shapescript_svg || fallback_svg
37
+ shapescript_svg.to_s
55
38
  end
56
39
 
57
40
  private
58
41
 
59
- # Try MDG-provided ShapeScript first.
42
+ # Renders MDG-provided ShapeScript for the stereotype.
60
43
  # @return [String, nil] SVG fragment from parsed ShapeScript
61
44
  def shapescript_svg
62
45
  return nil unless mdg_registry
@@ -83,35 +66,12 @@ module Ea
83
66
  nil
84
67
  end
85
68
 
86
- # @return [String] hardcoded fallback polygon, or "" if none
87
- def fallback_svg
88
- spec = FALLBACK_ICONS[stereotype_name]
89
- return "" unless spec
90
-
91
- fill, stroke, points = spec
92
- cx = bounds.x + bounds.width / 2 + OFFSET_X
93
- cy = bounds.y + bounds.height / 2 + OFFSET_Y
94
- translated = points.map { |x, y| translate_point(cx + x, cy + y) }
95
- pts_str = translated.map { |x, y| coord(x) + "," + coord(y) }.join(" ")
96
- %(<polygon points="#{pts_str}" fill="#{fill}" stroke="#{stroke}" stroke-width="1"/>)
97
- end
98
-
99
69
  def stereotype_name
100
70
  return nil unless classifier.is_a?(Ea::Model::Classifier)
101
71
 
102
72
  refs = classifier.stereotype_refs
103
73
  refs&.first
104
74
  end
105
-
106
- def translate_point(x, y)
107
- return [x, y] unless canvas
108
-
109
- [canvas.translate_x(x), canvas.translate_y(y)]
110
- end
111
-
112
- def coord(value)
113
- Ea::Svg::EaEmitter::Canvas.coord(value)
114
- end
115
75
  end
116
76
  end
117
77
  end
@@ -11,7 +11,7 @@ module Ea
11
11
  class Elements
12
12
  DEFAULT_FILL = "#FFFFFF"
13
13
  DEFAULT_STROKE = "#000000"
14
- DEFAULT_STROKE_WIDTH = 2
14
+ DEFAULT_STROKE_WIDTH = 1
15
15
  DEFAULT_TEXT_COLOR = "#000000"
16
16
 
17
17
  attr_reader :diagram, :model_index, :canvas, :document
@@ -178,6 +178,9 @@ module Ea
178
178
  off_canvas_parent_name: parent_name,
179
179
  foreign_package_name: foreign_package_name_for(classifier),
180
180
  suppress_stereotypes: suppress_stereotypes?) : []
181
+ header_lines = header_lines.map do |text, style|
182
+ [text, font_resolver.header_style_for(style, element)]
183
+ end
181
184
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
182
185
  tagged_values_for(classifier).size,
183
186
  header_lines, classifier: classifier, family: family,
@@ -257,9 +260,11 @@ module Ea
257
260
  # Bag': Set enum box stored h59 -> EA 223 fitting six
258
261
  # literals); op-only boxes keep the no-growth behavior
259
262
  # (op_bottom_y is a divider sentinel, not content).
263
+ # Saved boxes may already fit rows with less than the growth
264
+ # padding. Test the baseline for overflow before adding padding.
260
265
  if (attr_lines.any? || enum_literals_for(classifier).any?) &&
261
266
  grown_bottom.positive? &&
262
- grown_bottom + 8 > bounds.y + bounds.height
267
+ grown_bottom > bounds.y + bounds.height
263
268
  bounds = Ea::Model::Bounds.new(
264
269
  x: bounds.x, y: bounds.y,
265
270
  width: bounds.width,
@@ -43,7 +43,7 @@ module Ea
43
43
  return explicit
44
44
  end
45
45
 
46
- theme_family_for(explicit) || diagram_default_family || DEFAULT_FONT_FAMILY
46
+ default_family_for(explicit)
47
47
  end
48
48
 
49
49
  def size_for(element)
@@ -72,14 +72,29 @@ module Ea
72
72
  end
73
73
 
74
74
  def style_for(element)
75
- element&.font_italic ? "italic" : "normal"
75
+ return "italic" if element&.font_italic
76
+ return "normal" if element&.style&.key?(:italic)
77
+
78
+ diagram.hand_draw ? "italic" : "normal"
79
+ end
80
+
81
+ def header_style_for(style, element)
82
+ return style unless diagram.hand_draw && style_for(element) == "italic"
83
+
84
+ style == :bold ? :bold_italic : style
76
85
  end
77
86
 
78
87
  private
79
88
 
89
+ def default_family_for(explicit)
90
+ return "Source Sans Pro" if diagram.hand_draw
91
+
92
+ theme_family_for(explicit) || diagram_default_family || DEFAULT_FONT_FAMILY
93
+ end
94
+
80
95
  def theme_family_for(explicit)
81
96
  return unless theme.themed?
82
- return theme.font_family if DEFAULT_MARKER_FAMILIES.include?(explicit)
97
+ return theme.font_family || "Carlito" if DEFAULT_MARKER_FAMILIES.include?(explicit)
83
98
 
84
99
  theme.element_font_family || theme.font_family
85
100
  end
@@ -4,7 +4,9 @@ module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
6
  module Marker
7
- # Navigability arrow at the target end of an association.
7
+ # Navigability arrow at the target end of an association, plus
8
+ # the diamond of an association whose end carries an aggregate
9
+ # flag (SourceIsAggregate/DestIsAggregate).
8
10
  # Rendered as a `<path>` (not polygon) sharing style with the
9
11
  # connector line. When both ends are navigable
10
12
  # (bidirectional association), EA renders a separate arrow at
@@ -16,6 +18,14 @@ module Ea
16
18
 
17
19
  def self.specs_for(connector, source, target, before_target,
18
20
  after_source, relationship: nil)
21
+ Diamond.flagged_end_specs(relationship, source, target,
22
+ before_target, after_source) +
23
+ arrow_specs(connector, source, target, before_target,
24
+ after_source)
25
+ end
26
+
27
+ def self.arrow_specs(connector, source, target, before_target,
28
+ after_source)
19
29
  # EA draws the navigability arrow on directed
20
30
  # associations AND aggregations (333EFCCF: 6 aggregations
21
31
  # render diamond + path arrow); Direction
@@ -33,6 +43,7 @@ module Ea
33
43
  base = whole_end_at_source ? before_target : after_source
34
44
  [Registry::Spec.new(shape: :arrow, anchor: anchor, base: base)]
35
45
  end
46
+ private_class_method :arrow_specs
36
47
 
37
48
  # EA marks truly bidirectional associations with
38
49
  # direction="Bi-Directional" on t_connector. These get a
@@ -4,8 +4,9 @@ module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
6
  module Marker
7
- # EA endpoint fields select the diamond end; direction selects
8
- # the arrow end for target-owned aggregations.
7
+ # EA endpoint fields select the diamond end and its fill (shared
8
+ # is hollow, composite is solid); direction selects the arrow end
9
+ # for target-owned aggregations.
9
10
  class Diamond < Kind
10
11
  def self.handles?(effective_type)
11
12
  %w[Aggregation Composition].include?(effective_type)
@@ -15,7 +16,7 @@ module Ea
15
16
  whole_at_target = whole_at_target?(connector, relationship)
16
17
  whole_anchor = whole_at_target ? target : source
17
18
  whole_base = whole_at_target ? before_target : after_source
18
- specs = [Registry::Spec.new(shape: :diamond,
19
+ specs = [Registry::Spec.new(shape: diamond_shape(connector, relationship, whole_at_target),
19
20
  anchor: whole_anchor,
20
21
  base: whole_base)]
21
22
  return specs unless whole_at_target
@@ -24,6 +25,41 @@ module Ea
24
25
  before_target, after_source))
25
26
  end
26
27
 
28
+ # Diamonds for the ends of a plain association that carry an
29
+ # aggregate flag.
30
+ def self.flagged_end_specs(relationship, source, target,
31
+ before_target, after_source)
32
+ return [] unless relationship
33
+
34
+ { target_aggregation: [target, before_target],
35
+ source_aggregation: [source, after_source] }.filter_map do |reader, (anchor, base)|
36
+ shape = flagged_shape(relationship.public_send(reader))
37
+ Registry::Spec.new(shape: shape, anchor: anchor, base: base) if shape
38
+ end
39
+ end
40
+
41
+ def self.flagged_shape(kind)
42
+ case kind.to_s
43
+ when "shared" then :diamond_hollow
44
+ when "composite" then :diamond
45
+ end
46
+ end
47
+ private_class_method :flagged_shape
48
+
49
+ def self.diamond_shape(connector, relationship, whole_at_target)
50
+ kind = if relationship
51
+ whole_at_target ? relationship.target_aggregation : relationship.source_aggregation
52
+ end
53
+ kind = type_default_kind(connector) if kind.to_s.empty? || kind.to_s == "none"
54
+ kind.to_s == "shared" ? :diamond_hollow : :diamond
55
+ end
56
+ private_class_method :diamond_shape
57
+
58
+ def self.type_default_kind(connector)
59
+ connector.connector_type == "Composition" ? "composite" : "shared"
60
+ end
61
+ private_class_method :type_default_kind
62
+
27
63
  def self.whole_at_target?(connector, relationship)
28
64
  return true if dest_flagged?(relationship)
29
65
  return false if source_flagged?(relationship)
@@ -15,7 +15,7 @@ module Ea
15
15
  class OpenTriangle < Kind
16
16
  def self.handles?(effective_type)
17
17
  %w[Generalization Realization Realisation Dependency
18
- InformationFlow NoteLink].include?(effective_type)
18
+ InformationFlow].include?(effective_type)
19
19
  end
20
20
 
21
21
  def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
@@ -5,30 +5,17 @@ module Ea
5
5
  module EaEmitter
6
6
  module Marker
7
7
  # PackageImport marker for «import» connectors between
8
- # packages. EA draws a closed trapezoid silhouette at the
9
- # source end (the importing package) and an open V-arrow at
10
- # the target end (the imported package) — but ONLY when the
11
- # connector's geometry string carries explicit SX/SY offset
12
- # fields. Auto-routed connectors (no SX/SY) render as a
13
- # plain solid line with no package_anchor marker.
14
- #
15
- # Verified against both QEAs (output by the same EA run):
16
- # - basic.qea Package Imports: SX=0;SY=0 present → marker
17
- # - plateau Urban Planning ADE2: SX/SY absent → no marker
8
+ # packages. EA draws the dependency-style open arrowhead at the
9
+ # target (imported) end, whether or not the diagram link
10
+ # carries explicit SX/SY offsets.
18
11
  class PackageImport < Kind
19
12
  def self.handles?(effective_type)
20
13
  effective_type == "Package"
21
14
  end
22
15
 
23
- def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
24
- return [] unless connector.has_geometry_offsets
25
-
26
- [
27
- Registry::Spec.new(shape: :package_anchor,
28
- anchor: source, base: after_source),
29
- Registry::Spec.new(shape: :arrow, anchor: target,
30
- base: before_target)
31
- ]
16
+ def self.specs_for(_connector, _source, target, before_target, _after_source, **)
17
+ [Registry::Spec.new(shape: :dependency_arrow,
18
+ anchor: target, base: before_target)]
32
19
  end
33
20
  end
34
21
  end
@@ -4,16 +4,12 @@ module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
6
  module Marker
7
- # Plus-symbol marker at the contained end of a Nesting
8
- # connector. EA renders a "+" (16x16, two crossed lines)
9
- # at the end pointing INTO the container.
7
+ # Circled-plus marker at the container end of a Nesting
8
+ # connector. EA draws a white circle of radius ARM_LENGTH just
9
+ # outside the container edge with a plus (two crossed arms)
10
+ # through its centre; the connector starts at the nested
11
+ # element and ends at the container.
10
12
  #
11
- # Path encoding (two sub-paths in one <path>):
12
- #
13
- # M cx cy-8 L cx cy+8 (vertical arm, 16 tall)
14
- # M cx+8 cy L cx-8 cy (horizontal arm, 16 wide)
15
- #
16
- # cx, cy = plus center, anchored at the source-end waypoint.
17
13
  class Plus < Kind
18
14
  ARM_LENGTH = 8
19
15
 
@@ -22,14 +18,10 @@ module Ea
22
18
  end
23
19
 
24
20
  def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
25
- # Plus is at the contained (child) end. EA's convention:
26
- # source end is the child, target end is the parent.
27
- # Default to source unless the connector direction
28
- # reverses the ends.
29
- whole_end_at_source = whole_end_at_source?(connector)
30
- anchor = whole_end_at_source ? source : target
31
- base = whole_end_at_source ? after_source : before_target
32
- [Registry::Spec.new(shape: :plus, anchor: anchor, base: base)]
21
+ container_at_target = whole_end_at_source?(connector)
22
+ anchor = container_at_target ? target : source
23
+ base = container_at_target ? before_target : after_source
24
+ [Registry::Spec.new(shape: :nesting, anchor: anchor, base: base)]
33
25
  end
34
26
  end
35
27
  end