import { TYPE_KEYS, WIDGET_DEFAULTS } from './constants'; export function typeKeyFor(type) { return TYPE_KEYS[type] || 'label'; } export function invertLightness(hex) { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; const maxC = Math.max(r, g, b); const minC = Math.min(r, g, b); const delta = maxC - minC; let L = (maxC + minC) * 0.5; let S = 0, H = 0; if (delta > 0.0001) { S = L < 0.5 ? delta / (maxC + minC) : delta / (2 - maxC - minC); if (maxC === r) H = ((g - b) / delta + (g < b ? 6 : 0)) / 6; else if (maxC === g) H = ((b - r) / delta + 2) / 6; else H = ((r - g) / delta + 4) / 6; } L = 1 - L; const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; let ro, go, bo; if (S < 0.0001) { ro = go = bo = L; } else { const q = L < 0.5 ? L * (1 + S) : L + S - L * S; const p = 2 * L - q; ro = hue2rgb(p, q, H + 1/3); go = hue2rgb(p, q, H); bo = hue2rgb(p, q, H - 1/3); } const toHex = v => Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16).padStart(2, '0'); return `#${toHex(ro)}${toHex(go)}${toHex(bo)}`; } export function clamp(value, min, max) { if (Number.isNaN(value)) return min; return Math.max(min, Math.min(max, value)); } export function minSizeFor(widget) { const key = typeKeyFor(widget.type); if (key === 'button') return { w: 60, h: 30 }; if (key === 'led') return { w: 20, h: 20 }; if (key === 'icon') return { w: 24, h: 24 }; if (key === 'tabview') return { w: 100, h: 100 }; if (key === 'powerflow') return { w: 240, h: 180 }; if (key === 'powernode') return { w: 70, h: 70 }; if (key === 'powerlink') return { w: 1, h: 1 }; if (key === 'chart') return { w: 160, h: 120 }; if (key === 'roomcard') return { w: 120, h: 120 }; if (key === 'rectangle') return { w: 40, h: 30 }; if (key === 'arc') return { w: 80, h: 80 }; if (key === 'buttonmatrix') return { w: 120, h: 60 }; return { w: 40, h: 20 }; } export function hexToRgba(hex, alpha) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r},${g},${b},${alpha})`; } export function normalizeWidget(w, nextWidgetIdRef) { const key = typeKeyFor(w.type); const defaults = WIDGET_DEFAULTS[key]; Object.keys(defaults).forEach((prop) => { if (prop === 'shadow' || prop === 'conditions') return; if (w[prop] === undefined || w[prop] === null) { w[prop] = defaults[prop]; } }); // Conditions: ensure each widget has its own array if (defaults.conditions !== undefined) { if (!Array.isArray(w.conditions)) { w.conditions = []; } } // SubButtons: ensure each widget has its own array if (defaults.subButtons !== undefined) { if (!Array.isArray(w.subButtons)) { w.subButtons = []; } // Ensure subButtonSize has a default if (w.subButtonSize === undefined || w.subButtonSize === null) { w.subButtonSize = defaults.subButtonSize || 40; } // Ensure subButtonDistance has a default if (w.subButtonDistance === undefined || w.subButtonDistance === null) { w.subButtonDistance = defaults.subButtonDistance || 80; } // Ensure subButtonOpacity has a default if (w.subButtonOpacity === undefined || w.subButtonOpacity === null) { w.subButtonOpacity = defaults.subButtonOpacity || 255; } // Ensure cardStyle has a default if (w.cardStyle === undefined || w.cardStyle === null) { w.cardStyle = defaults.cardStyle || 0; } } // TextLines: ensure each widget has its own array and each line has fontSize if (defaults.textLines !== undefined) { if (!Array.isArray(w.textLines)) { w.textLines = []; } // Ensure each textLine has a fontSize w.textLines.forEach(tl => { if (tl.fontSize === undefined || tl.fontSize === null) { tl.fontSize = 1; // Default 18px } }); } if (!w.shadow) { w.shadow = { ...defaults.shadow }; } else { Object.keys(defaults.shadow).forEach((prop) => { if (w.shadow[prop] === undefined || w.shadow[prop] === null) { w.shadow[prop] = defaults.shadow[prop]; } }); } // Border style defaults (used by rectangle widget, harmless for others) if (w.borderWidth === undefined || w.borderWidth === null) { w.borderWidth = defaults.borderWidth ?? 0; } if (!w.borderColor) { w.borderColor = defaults.borderColor || '#ffffff'; } if (w.borderOpacity === undefined || w.borderOpacity === null) { w.borderOpacity = defaults.borderOpacity ?? 0; } if (defaults.chart) { const maxSeries = 3; if (!w.chart) { w.chart = { period: defaults.chart.period ?? 0, series: (defaults.chart.series || []).map(s => ({ ...s })) }; } else { if (w.chart.period === undefined || w.chart.period === null) { w.chart.period = defaults.chart.period ?? 0; } if (w.chart.showXLine === undefined) w.chart.showXLine = true; if (w.chart.showYLine === undefined) w.chart.showYLine = true; if (w.chart.showXLabels === undefined) w.chart.showXLabels = true; if (w.chart.showYLabels === undefined) w.chart.showYLabels = true; if (w.chart.showBg === undefined) w.chart.showBg = true; if (w.chart.showGrid === undefined) w.chart.showGrid = true; if (w.chart.lineWidth === undefined) w.chart.lineWidth = 2; if (w.chart.pointCount === undefined) w.chart.pointCount = 120; if (w.chart.showPoints === undefined) w.chart.showPoints = false; if (w.chart.pointSize === undefined) w.chart.pointSize = 4; if (!Array.isArray(w.chart.series)) { w.chart.series = []; } if (w.chart.series.length === 0 && defaults.chart.series) { w.chart.series = defaults.chart.series.map(s => ({ ...s })); } for (let i = 0; i < w.chart.series.length && i < maxSeries; i++) { const fallback = (defaults.chart.series && defaults.chart.series[i]) || defaults.chart.series?.[0] || { knxAddr: 0, textSrc: 1, color: '#EF6351' }; const entry = w.chart.series[i]; if (entry.knxAddr === undefined || entry.knxAddr === null) entry.knxAddr = fallback.knxAddr; if (entry.textSrc === undefined || entry.textSrc === null) entry.textSrc = fallback.textSrc; if (!entry.color) entry.color = fallback.color; } if (w.chart.series.length > maxSeries) { w.chart.series = w.chart.series.slice(0, maxSeries); } } } if (w.visible === undefined || w.visible === null) w.visible = true; if (w.x === undefined || w.x === null) w.x = 100; if (w.y === undefined || w.y === null) w.y = 100; if (w.id === undefined || w.id === null) { if (nextWidgetIdRef) w.id = nextWidgetIdRef.value++; } // Action defaults if (w.action === undefined) w.action = 0; // BUTTON_ACTIONS.KNX if (w.targetScreen === undefined) w.targetScreen = 0; // Icon defaults if (w.iconCodepoint === undefined) w.iconCodepoint = defaults.iconCodepoint || 0; if (w.iconPosition === undefined) w.iconPosition = defaults.iconPosition || 0; if (w.iconSize === undefined) w.iconSize = defaults.iconSize || 1; if (w.iconGap === undefined) w.iconGap = defaults.iconGap || 8; if (w.iconPositionX === undefined) w.iconPositionX = defaults.iconPositionX || 0; if (w.iconPositionY === undefined) w.iconPositionY = defaults.iconPositionY || 0; // Hierarchy if (w.parentId === undefined) w.parentId = -1; if (key === 'button' && (w.isContainer === undefined || w.isContainer === null)) { w.isContainer = defaults.isContainer ?? false; } } export function normalizeScreen(screen, nextScreenIdRef, nextWidgetIdRef) { if (screen.id === undefined || screen.id === null) { if (nextScreenIdRef) screen.id = nextScreenIdRef.value++; } if (!screen.name) screen.name = `Screen ${screen.id}`; if (screen.mode === undefined || screen.mode === null) screen.mode = 0; if (!screen.bgColor) screen.bgColor = '#1A1A2E'; if (!screen.nightBgColor) screen.nightBgColor = '#0E1217'; if (screen.bgImage === undefined) screen.bgImage = ''; if (screen.bgImageMode === undefined) screen.bgImageMode = 1; if (!Array.isArray(screen.widgets)) screen.widgets = []; // Modal defaults if (!screen.modal) { screen.modal = { x: 0, y: 0, w: 0, h: 0, radius: 12, dim: true }; } else { if (screen.modal.x === undefined) screen.modal.x = 0; if (screen.modal.y === undefined) screen.modal.y = 0; if (screen.modal.w === undefined) screen.modal.w = 0; if (screen.modal.h === undefined) screen.modal.h = 0; if (screen.modal.radius === undefined) screen.modal.radius = 12; if (screen.modal.dim === undefined) screen.modal.dim = true; } screen.widgets.forEach(w => normalizeWidget(w, nextWidgetIdRef)); }