UNPKG

@etm-professional-control/winccoa-mcp-server

Version:

MCP Server for WinCC OA with field-specific configurations

196 lines 7.42 kB
/** * Layout Helper * * Helper class for resolving widget layouts, finding positions, and validating grid placements. */ import { isGridPosition, getWidgetPresets, getDefaultDimensions, DASHBOARD_GRID } from '../../types/dashboards/layout.js'; /** * Layout Helper Class */ export class LayoutHelper { /** * Resolve layout configuration to concrete grid position with dimensions * @param layout - Layout configuration (preset or explicit position) * @param widgetType - Type of widget * @param existingWidgets - Array of existing widgets on dashboard * @returns Complete widget dimensions */ resolveLayout(layout, widgetType, existingWidgets) { // Default to 'auto' if no layout specified const resolvedLayout = layout || 'auto'; // If explicit grid position provided if (isGridPosition(resolvedLayout)) { const presets = getWidgetPresets(widgetType); const defaultPreset = getDefaultDimensions(widgetType); return { x: resolvedLayout.x, y: resolvedLayout.y, cols: resolvedLayout.cols, rows: resolvedLayout.rows, minCols: defaultPreset.minCols, minRows: defaultPreset.minRows }; } // Layout is a preset name const preset = resolvedLayout; // Get dimensions for the preset const dimensions = this.getPresetDimensions(preset, widgetType); // If 'auto', find best position automatically if (preset === 'auto') { const position = this.findAutoPosition(existingWidgets, dimensions); return { ...position, cols: dimensions.cols, rows: dimensions.rows, minCols: dimensions.minCols, minRows: dimensions.minRows }; } // For other presets, start at position (0, 0) unless finding a better spot const position = this.findAutoPosition(existingWidgets, dimensions); return { ...position, cols: dimensions.cols, rows: dimensions.rows, minCols: dimensions.minCols, minRows: dimensions.minRows }; } /** * Get preset dimensions for a widget type * @param preset - Preset name * @param widgetType - Widget type * @returns Widget size preset with dimensions */ getPresetDimensions(preset, widgetType) { // For 'auto', use default (medium) dimensions if (preset === 'auto') { return getDefaultDimensions(widgetType); } const presets = getWidgetPresets(widgetType); const dimensions = presets[preset]; if (!dimensions) { console.warn(`Preset '${preset}' not available for widget type '${widgetType}', using default`); return getDefaultDimensions(widgetType); } return dimensions; } /** * Align a coordinate to the grid alignment * @param value - Coordinate value * @returns Aligned coordinate (multiple of DASHBOARD_GRID.ALIGNMENT) */ alignToGrid(value) { return Math.round(value / DASHBOARD_GRID.ALIGNMENT) * DASHBOARD_GRID.ALIGNMENT; } /** * Find next available position in grid for a widget * Uses simple top-to-bottom, left-to-right placement with 4-column grid alignment * @param existingWidgets - Array of existing widgets * @param widgetSize - Size of widget to place * @returns Grid position {x, y} */ findAutoPosition(existingWidgets, widgetSize) { // If no widgets, start at top-left if (existingWidgets.length === 0) { return { x: 0, y: 0 }; } // Try to find a position by scanning the grid with alignment const maxY = this.getMaxY(existingWidgets) + 20; // Scan up to 20 rows below last widget for (let y = 0; y <= maxY; y++) { // Increment x by ALIGNMENT (4) for grid alignment for (let x = 0; x <= DASHBOARD_GRID.TOTAL_COLUMNS - widgetSize.cols; x += DASHBOARD_GRID.ALIGNMENT) { const position = { x, y }; if (this.isPositionAvailable(position, widgetSize, existingWidgets)) { // Apply grid alignment to the found position return { x: this.alignToGrid(position.x), y: position.y }; } } } // If no position found (shouldn't happen), place below all widgets return { x: 0, y: maxY + 1 }; } /** * Check if a position is available (no overlap with existing widgets) * @param position - Position to check * @param size - Size of widget * @param existingWidgets - Existing widgets on dashboard * @returns true if position is available */ isPositionAvailable(position, size, existingWidgets) { const newWidget = { x: position.x, y: position.y, cols: size.cols, rows: size.rows }; // Check against all existing widgets for overlap for (const widget of existingWidgets) { if (this.widgetsOverlap(newWidget, widget)) { return false; } } // Check if widget fits within grid bounds return newWidget.x + newWidget.cols <= DASHBOARD_GRID.TOTAL_COLUMNS; } /** * Check if two widgets overlap * @param w1 - First widget * @param w2 - Second widget * @returns true if widgets overlap */ widgetsOverlap(w1, w2) { return !(w1.x + w1.cols <= w2.x || // w1 is left of w2 w2.x + w2.cols <= w1.x || // w2 is left of w1 w1.y + w1.rows <= w2.y || // w1 is above w2 w2.y + w2.rows <= w1.y // w2 is above w1 ); } /** * Get maximum Y coordinate (bottom edge) of existing widgets * @param widgets - Array of widgets * @returns Maximum Y + rows */ getMaxY(widgets) { if (widgets.length === 0) return 0; return Math.max(...widgets.map((w) => w.y + w.rows)); } /** * Validate grid position * @param position - Position to validate * @returns true if valid */ validateGridPosition(position) { // Check bounds if (position.x < 0 || position.y < 0) { return false; } if (position.cols < DASHBOARD_GRID.MIN_SIZE || position.rows < DASHBOARD_GRID.MIN_SIZE) { return false; } if (position.x + position.cols > DASHBOARD_GRID.TOTAL_COLUMNS) { return false; } return true; } /** * Suggest next position for a widget (helper for user) * @param existingWidgets - Existing widgets * @param widgetType - Type of widget to place * @param preset - Optional preset * @returns Suggested grid position */ suggestNextPosition(existingWidgets, widgetType, preset = 'auto') { const dimensions = this.getPresetDimensions(preset === 'auto' ? 'medium' : preset, widgetType); const position = this.findAutoPosition(existingWidgets, dimensions); return { ...position, ...dimensions }; } } //# sourceMappingURL=LayoutHelper.js.map