Class: Lutaml::Ea::Diagram::LayoutEngine

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

Overview

Layout engine for positioning diagram elements

This engine calculates optimal positions for diagram elements based on their relationships and EA diagram data. It handles automatic layout for elements that don’t have explicit positions.

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

Constructor Details

#initialize(options = {}) ⇒ LayoutEngine

Returns a new instance of LayoutEngine.



23
24
25
26
27
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 23

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.



21
22
23
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 21

def element_height
  @element_height
end

#element_widthObject (readonly)

Returns the value of attribute element_width.



21
22
23
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 21

def element_width
  @element_width
end

#spacingObject (readonly)

Returns the value of attribute spacing.



21
22
23
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 21

def spacing
  @spacing
end

Instance Method Details

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

Apply automatic layout to elements without positions

Parameters:

  • elements (Array)

    Array of diagram elements

  • connectors (Array) (defaults to: [])

    Array of connectors

Returns:

  • (Array)

    Elements with calculated positions



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

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

  # Apply force-directed layout for unpositioned elements
  if unpositioned_elements.any?
    positioned_elements += apply_force_directed_layout(
      unpositioned_elements,
      connectors,
      positioned_elements,
    )
  end

  positioned_elements
end

#apply_padding_to_bounds(bounds) ⇒ Hash

Apply padding to bounds

Parameters:

  • bounds (Hash)

    Bounds with x, y, width, height

Returns:

  • (Hash)

    Padded bounds



59
60
61
62
63
64
65
66
67
68
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 59

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) ⇒ Hash

Calculate bounds for the entire diagram

Parameters:

  • diagram_data (Hash)

    Diagram data with elements and connectors

Returns:

  • (Hash)

    Bounds with x, y, width, height



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 32

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?

  # Find min/max coordinates
  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) ⇒ Hash?

Calculate min/max bounds from connector endpoints

Parameters:

  • connectors (Array)

    Array of connectors with geometry

Returns:

  • (Hash, nil)

    Bounds hash with min_x, max_x, min_y, max_y



160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 160

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 = []) ⇒ Hash

Calculate optimal position for a single element

Parameters:

  • element (Hash)

    Element data

  • related_elements (Array) (defaults to: [])

    Related elements

Returns:

  • (Hash)

    Element with calculated position



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 95

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

  # Simple positioning: place to the right of related elements
  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

#convert_ea_coordinates(diagram_object) ⇒ Object

Deprecated.

Use DiagramPresenter coordinate handling instead

Convert EA coordinates (deprecated - now handled by DiagramPresenter)



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

def convert_ea_coordinates(diagram_object)
  left = diagram_object.left || 0
  top = diagram_object.top || 0
  right = diagram_object.right || 100
  bottom = diagram_object.bottom || 100

  {
    x: left,
    y: top,
    width: right - left,
    height: bottom - top,
  }
end

#normalize_coordinates(elements) ⇒ Object

Deprecated.

No longer needed in current architecture

Normalize coordinates (deprecated)



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/lutaml/ea/diagram/layout_engine.rb', line 135

def normalize_coordinates(elements) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
  return elements if elements.empty?

  largest_negative_x = elements.map { |e| e[:x] || 0 }.min
  largest_negative_y = elements.map { |e| e[:y] || 0 }.min
  offset_x = largest_negative_x.negative? ? -largest_negative_x : 0
  offset_y = largest_negative_y.negative? ? -largest_negative_y : 0

  return elements if offset_x.zero? && offset_y.zero?

  elements.each do |e|
    e[:x] = 0 if e[:x].nil?
    e[:y] = 0 if e[:y].nil?
    e[:width] = 0 if e[:width].nil?
    e[:height] = 0 if e[:height].nil?
    e[:x] = e[:x].to_i + offset_x
    e[:y] = e[:y].to_i + offset_y
    e[:width] = -e[:width] if e[:width].negative?
    e[:height] = -e[:height] if e[:height].negative?
  end
end