Class: Ea::Diagram::LayoutEngine

Inherits:
Object
  • Object
show all
Includes:
Util
Defined in:
lib/ea/diagram/layout_engine.rb

Overview

Layout engine for positioning diagram elements

Constant Summary collapse

DEFAULT_SPACING =
50
DEFAULT_PADDING =
20
ELEMENT_WIDTH =
120
ELEMENT_HEIGHT =
80

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Util

#parse_ea_geometry, #parse_geometry_offsets, #style_to_css

Constructor Details

#initialize(options = {}) ⇒ LayoutEngine

Returns a new instance of LayoutEngine.



16
17
18
19
20
# File 'lib/ea/diagram/layout_engine.rb', line 16

def initialize(options = {})
  @spacing = options[:spacing] || DEFAULT_SPACING
  @element_width = options[:element_width] || ELEMENT_WIDTH
  @element_height = options[:element_height] || ELEMENT_HEIGHT
end

Instance Attribute Details

#element_heightObject (readonly)

Returns the value of attribute element_height.



14
15
16
# File 'lib/ea/diagram/layout_engine.rb', line 14

def element_height
  @element_height
end

#element_widthObject (readonly)

Returns the value of attribute element_width.



14
15
16
# File 'lib/ea/diagram/layout_engine.rb', line 14

def element_width
  @element_width
end

#spacingObject (readonly)

Returns the value of attribute spacing.



14
15
16
# File 'lib/ea/diagram/layout_engine.rb', line 14

def spacing
  @spacing
end

Instance Method Details

#apply_force_directed_layout(elements, _connectors, fixed_elements) ⇒ Object

rubocop:disable Metrics/AbcSize,Metrics:MethodLength



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ea/diagram/layout_engine.rb', line 147

def apply_force_directed_layout(elements, _connectors, fixed_elements) # rubocop:disable Metrics/AbcSize,Metrics:MethodLength
  positioned = []
  elements.each_with_index do |element, index|
    cols = Math.sqrt(elements.size).ceil
    row = index / cols
    col = index % cols

    x = col * (ELEMENT_WIDTH + spacing)
    y = row * (ELEMENT_HEIGHT + spacing)

    if fixed_elements.any?
      x += fixed_elements.map do |e|
        (e[:x] || 0) + element_width_for(e)
      end.max + (spacing * 2)
    end

    positioned << element.merge(x: x, y: y)
  end

  positioned
end

#apply_layout(elements, connectors = []) ⇒ Object

rubocop:disable Metrics/MethodLength



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/ea/diagram/layout_engine.rb', line 56

def apply_layout(elements, connectors = []) # rubocop:disable Metrics/MethodLength
  positioned_elements, unpositioned_elements = elements.partition do |e|
    e[:x] && e[:y]
  end

  if unpositioned_elements.any?
    positioned_elements += apply_force_directed_layout(
      unpositioned_elements,
      connectors,
      positioned_elements,
    )
  end

  positioned_elements
end

#apply_padding_to_bounds(bounds) ⇒ Object

rubocop:disable Metrics/AbcSize



45
46
47
48
49
50
51
52
53
54
# File 'lib/ea/diagram/layout_engine.rb', line 45

def apply_padding_to_bounds(bounds) # rubocop:disable Metrics/AbcSize
  padding_x = [bounds[:width] * 0.05, DEFAULT_PADDING].max
  padding_y = [bounds[:height] * 0.05, DEFAULT_PADDING].max
  {
    x: bounds[:x] - padding_x,
    y: bounds[:y] - padding_y,
    width: bounds[:width] + (padding_x * 2),
    height: bounds[:height] + (padding_y * 2),
  }
end

#calculate_bounds(diagram_data) ⇒ Object

rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/ea/diagram/layout_engine.rb', line 22

def calculate_bounds(diagram_data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
  elements = diagram_data[:elements] || []
  return { x: 0, y: 0, width: 400, height: 300 } if elements.empty?

  min_x = elements.map { |e| e[:x] || 0 }.min
  min_y = elements.map { |e| e[:y] || 0 }.min
  max_x = elements.map do |e|
    (e[:x] || 0) + element_width_for(e)
  end.max
  max_y = elements.map do |e|
    (e[:y] || 0) + element_height_for(e)
  end.max

  apply_padding_to_bounds(
    {
      x: min_x,
      y: min_y,
      width: max_x - min_x,
      height: max_y - min_y,
    },
  )
end

#calculate_connector_bounds(connectors) ⇒ Object

rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity



89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ea/diagram/layout_engine.rb', line 89

def calculate_connector_bounds(connectors) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  return nil if connectors.empty?

  valid = connectors.select do |c|
    c[:source_element] && c[:target_element] && c[:geometry]
  end
  return nil if valid.empty?

  points = valid.flat_map { |conn| connector_endpoints(conn) }
  xs = points.map(&:first)
  ys = points.map(&:last)

  { min_x: xs.min, max_x: xs.max, min_y: ys.min, max_y: ys.max }
end

#calculate_element_position(element, related_elements = []) ⇒ Object

rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/ea/diagram/layout_engine.rb', line 72

def calculate_element_position(element, related_elements = []) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
  return element if element[:x] && element[:y]

  if related_elements.any?
    max_x = related_elements.map do |e|
      (e[:x] || 0) + element_width_for(e)
    end.max
    element[:x] = max_x + spacing
    element[:y] = related_elements.first[:y] || 0
  else
    element[:x] = 0
    element[:y] = 0
  end

  element
end

#connector_endpoints(conn) ⇒ Object

rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ea/diagram/layout_engine.rb', line 104

def connector_endpoints(conn) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  src = conn[:source_element]
  tgt = conn[:target_element]
  sx, sy, ex, ey = parse_geometry_offsets(conn[:geometry])

  src_point = [(src[:x] || 0) + (src[:width] || 120) + sx,
               (src[:y] || 0) + ((src[:height] || 80) / 2) + sy]
  tgt_point = [(tgt[:x] || 0) + ex,
               (tgt[:y] || 0) + ((tgt[:height] || 80) / 2) + ey]

  [src_point, tgt_point]
end

#element_height_for(element) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/ea/diagram/layout_engine.rb', line 132

def element_height_for(element)
  if element[:height]
    return element[:height].zero? ? ELEMENT_HEIGHT : element[:height]
  end

  case element[:type]
  when "class"
    (element[:operations]&.size.to_i * 15) + ELEMENT_HEIGHT
  when "package"
    ELEMENT_HEIGHT - 10
  else
    ELEMENT_HEIGHT
  end
end

#element_width_for(element) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/ea/diagram/layout_engine.rb', line 117

def element_width_for(element)
  if element[:width]
    return element[:width].zero? ? ELEMENT_WIDTH : element[:width]
  end

  case element[:type]
  when "class"
    (element[:attributes]&.size.to_i * 10) + ELEMENT_WIDTH
  when "package"
    ELEMENT_WIDTH + 20
  else
    ELEMENT_WIDTH
  end
end