class Lutaml::Ea::Diagram::LayoutEngine
automatic layout for elements that don’t have explicit positions.
based on their relationships and EA diagram data. It handles
This engine calculates optimal positions for diagram elements
Layout engine for positioning diagram elements
def apply_force_directed_layout(elements, _connectors, fixed_elements) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
def apply_force_directed_layout(elements, _connectors, fixed_elements) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength # Simple force-directed layout implementation # In a real implementation, this would use iterative relaxation positioned = [] elements.each_with_index do |element, index| # Start with a grid-like initial position cols = Math.sqrt(elements.size).ceil row = index / cols col = index % cols x = col * (ELEMENT_WIDTH + spacing) y = row * (ELEMENT_HEIGHT + spacing) # Adjust based on fixed elements 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
def apply_layout(elements, connectors = []) # rubocop:disable Metrics/MethodLength
-
(Array)- Elements with calculated positions
Parameters:
-
connectors(Array) -- Array of connectors -
elements(Array) -- Array of diagram elements
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
def apply_padding_to_bounds(bounds) # rubocop:disable Metrics/AbcSize
-
(Hash)- Padded bounds
Parameters:
-
bounds(Hash) -- Bounds with x, y, width, height
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
def calculate_bounds(diagram_data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
-
(Hash)- Bounds with x, y, width, height
Parameters:
-
diagram_data(Hash) -- Diagram data with elements and connectors
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
def calculate_connector_bounds(connectors) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
-
(Hash, nil)- Bounds hash with min_x, max_x, min_y, max_y
Parameters:
-
connectors(Array) -- Array of connectors with geometry
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
def calculate_element_position(element, related_elements = []) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
-
(Hash)- Element with calculated position
Parameters:
-
related_elements(Array) -- Related elements -
element(Hash) -- Element data
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
def connector_endpoints(conn) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
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
def convert_ea_coordinates(diagram_object)
- Use DiagramPresenter coordinate handling instead
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
def element_height_for(element) # rubocop:disable Metrics/MethodLength
def element_height_for(element) # rubocop:disable Metrics/MethodLength if element[:height] return element[:height].zero? ? ELEMENT_HEIGHT : element[:height] end # Could be customized based on element content case element[:type] when "class" (element[:operations]&.size.to_i * 15) + ELEMENT_HEIGHT when "package" ELEMENT_HEIGHT - 10 else ELEMENT_HEIGHT end end
def element_width_for(element) # rubocop:disable Metrics/MethodLength
def element_width_for(element) # rubocop:disable Metrics/MethodLength if element[:width] return element[:width].zero? ? ELEMENT_WIDTH : element[:width] end # Could be customized based on element content case element[:type] when "class" (element[:attributes]&.size.to_i * 10) + ELEMENT_WIDTH when "package" ELEMENT_WIDTH + 20 else ELEMENT_WIDTH end end
def initialize(options = {})
def initialize(options = {}) @spacing = options[:spacing] || DEFAULT_SPACING @element_width = options[:element_width] || ELEMENT_WIDTH @element_height = options[:element_height] || ELEMENT_HEIGHT end
def normalize_coordinates(elements) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
- No longer needed in current architecture
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