function logicalSize(record) { if (!record) return { width: 0, height: 0 }; const width = Number(record.width); const height = Number(record.height); const scale = Number(record.scale); const transform = Number(record.transform); if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(scale) || scale <= 0) return { width: 0, height: 0 }; const rotated = transform === 1 || transform === 3; return { width: (rotated ? height : width) / scale, height: (rotated ? width : height) / scale }; } function validCoordinate(value) { return Number.isFinite(value) && Number.isInteger(value) && value >= -100000 && value <= 100000; } function validate(layout) { if (!Array.isArray(layout) || layout.length === 0) return false; const names = {}; let primaryCount = 0; for (const record of layout) { if (!record || typeof record.name !== "string" || !/^[A-Za-z0-9_.-]+$/.test(record.name) || names[record.name]) return false; names[record.name] = true; if (!Number.isFinite(record.width) || record.width <= 0 || !Number.isFinite(record.height) || record.height <= 0 || !Number.isFinite(record.scale) || record.scale <= 0 || !Number.isInteger(record.transform) || record.transform < 0 || record.transform > 3 || !validCoordinate(record.x) || !validCoordinate(record.y) || typeof record.primary !== "boolean") return false; const size = logicalSize(record); if (!Number.isFinite(size.width) || size.width <= 0 || !Number.isFinite(size.height) || size.height <= 0) return false; if (record.primary) primaryCount += 1; } return primaryCount === 1; } function cloneLayout(layout) { return (layout || []).map(record => Object.assign({}, record)); } function normalize(layout) { const result = cloneLayout(layout); const primary = result.find(record => record.primary === true); if (!primary) return result; const anchorX = primary.x; const anchorY = primary.y; for (const record of result) { record.x -= anchorX; record.y -= anchorY; } return result; } function bounds(layout) { if (!Array.isArray(layout) || layout.length === 0) return { x: 0, y: 0, width: 0, height: 0 }; let left = Infinity; let top = Infinity; let right = -Infinity; let bottom = -Infinity; for (const record of layout) { const size = logicalSize(record); left = Math.min(left, record.x); top = Math.min(top, record.y); right = Math.max(right, record.x + size.width); bottom = Math.max(bottom, record.y + size.height); } return { x: left, y: top, width: right - left, height: bottom - top }; } function snap(layout, movingName, threshold) { const result = cloneLayout(layout); const moving = result.find(record => record.name === movingName); if (!moving) return result; const limit = Number.isFinite(threshold) && threshold >= 0 ? threshold : 16; const movingSize = logicalSize(moving); const movingXEdges = [moving.x, moving.x + movingSize.width]; const movingYEdges = [moving.y, moving.y + movingSize.height]; const stationary = result .filter(record => record.name !== movingName) .sort((left, right) => left.name.localeCompare(right.name)); let bestX = null; let bestY = null; for (const record of stationary) { const size = logicalSize(record); const xEdges = [record.x, record.x + size.width]; const yEdges = [record.y, record.y + size.height]; for (const source of movingXEdges) { for (const target of xEdges) { const delta = target - source; const distance = Math.abs(delta); if (distance <= limit && (bestX === null || distance < bestX.distance)) bestX = { delta, distance }; } } for (const source of movingYEdges) { for (const target of yEdges) { const delta = target - source; const distance = Math.abs(delta); if (distance <= limit && (bestY === null || distance < bestY.distance)) bestY = { delta, distance }; } } } if (bestX !== null) moving.x += bestX.delta; if (bestY !== null) moving.y += bestY.delta; return result; } function canvasRects(layout, canvasWidth, canvasHeight, padding) { const desktopBounds = bounds(layout); const inset = Math.max(0, Number(padding) || 0); const availableWidth = Math.max(0, Number(canvasWidth) - inset * 2); const availableHeight = Math.max(0, Number(canvasHeight) - inset * 2); const scale = desktopBounds.width > 0 && desktopBounds.height > 0 ? Math.min(availableWidth / desktopBounds.width, availableHeight / desktopBounds.height) : 0; const contentWidth = desktopBounds.width * scale; const contentHeight = desktopBounds.height * scale; const originX = inset + (availableWidth - contentWidth) / 2; const originY = inset + (availableHeight - contentHeight) / 2; return { bounds: desktopBounds, scale, rects: (layout || []).map(record => { const size = logicalSize(record); return { name: record.name, x: originX + (record.x - desktopBounds.x) * scale, y: originY + (record.y - desktopBounds.y) * scale, width: size.width * scale, height: size.height * scale, primary: record.primary === true }; }) }; }