/** * Selection Transforms — align, distribute, arrange, and match-size * operations on canvas shape elements. * * Each function accepts an array of shape elements (with x, y, width, height) * and mutates their positions in place. * * Exposed on `window.__miSelectionTransforms` so the MiActionExecutor can * invoke them by name. */ interface ShapeEl { x: number; y: number; width: number; height: number; } // ── Align ── export function alignLeft(shapes: ShapeEl[]) { if (shapes.length < 2) return; const minX = Math.min(...shapes.map((s) => s.x)); for (const s of shapes) s.x = minX; } export function alignRight(shapes: ShapeEl[]) { if (shapes.length < 2) return; const maxRight = Math.max(...shapes.map((s) => s.x + s.width)); for (const s of shapes) s.x = maxRight - s.width; } export function alignCenterH(shapes: ShapeEl[]) { if (shapes.length < 2) return; const centers = shapes.map((s) => s.x + s.width / 2); const avg = centers.reduce((a, b) => a + b, 0) / centers.length; for (const s of shapes) s.x = avg - s.width / 2; } export function alignTop(shapes: ShapeEl[]) { if (shapes.length < 2) return; const minY = Math.min(...shapes.map((s) => s.y)); for (const s of shapes) s.y = minY; } export function alignBottom(shapes: ShapeEl[]) { if (shapes.length < 2) return; const maxBottom = Math.max(...shapes.map((s) => s.y + s.height)); for (const s of shapes) s.y = maxBottom - s.height; } export function alignCenterV(shapes: ShapeEl[]) { if (shapes.length < 2) return; const centers = shapes.map((s) => s.y + s.height / 2); const avg = centers.reduce((a, b) => a + b, 0) / centers.length; for (const s of shapes) s.y = avg - s.height / 2; } // ── Distribute ── export function distributeH(shapes: ShapeEl[]) { if (shapes.length < 3) return; const sorted = [...shapes].sort((a, b) => a.x - b.x); const first = sorted[0]; const last = sorted[sorted.length - 1]; const totalWidth = sorted.reduce((s, el) => s + el.width, 0); const span = last.x + last.width - first.x; const gap = (span - totalWidth) / (sorted.length - 1); let cursor = first.x; for (const s of sorted) { s.x = cursor; cursor += s.width + gap; } } export function distributeV(shapes: ShapeEl[]) { if (shapes.length < 3) return; const sorted = [...shapes].sort((a, b) => a.y - b.y); const first = sorted[0]; const last = sorted[sorted.length - 1]; const totalHeight = sorted.reduce((s, el) => s + el.height, 0); const span = last.y + last.height - first.y; const gap = (span - totalHeight) / (sorted.length - 1); let cursor = first.y; for (const s of sorted) { s.y = cursor; cursor += s.height + gap; } } // ── Arrange ── export function arrangeRow(shapes: ShapeEl[]) { if (shapes.length < 2) return; const gap = 30; const baseY = Math.min(...shapes.map((s) => s.y)); let cursor = shapes[0].x; for (const s of shapes) { s.x = cursor; s.y = baseY; cursor += s.width + gap; } } export function arrangeColumn(shapes: ShapeEl[]) { if (shapes.length < 2) return; const gap = 30; const baseX = Math.min(...shapes.map((s) => s.x)); let cursor = shapes[0].y; for (const s of shapes) { s.x = baseX; s.y = cursor; cursor += s.height + gap; } } export function arrangeGrid(shapes: ShapeEl[]) { if (shapes.length < 2) return; const cols = Math.ceil(Math.sqrt(shapes.length)); const gap = 30; const baseX = Math.min(...shapes.map((s) => s.x)); const baseY = Math.min(...shapes.map((s) => s.y)); const maxW = Math.max(...shapes.map((s) => s.width)); const maxH = Math.max(...shapes.map((s) => s.height)); for (let i = 0; i < shapes.length; i++) { const col = i % cols; const row = Math.floor(i / cols); shapes[i].x = baseX + col * (maxW + gap); shapes[i].y = baseY + row * (maxH + gap); } } export function arrangeCircle(shapes: ShapeEl[]) { if (shapes.length < 2) return; const cx = shapes.reduce((s, el) => s + el.x + el.width / 2, 0) / shapes.length; const cy = shapes.reduce((s, el) => s + el.y + el.height / 2, 0) / shapes.length; const maxDim = Math.max(...shapes.map((s) => Math.max(s.width, s.height))); const radius = Math.max(maxDim * shapes.length / (2 * Math.PI), 150); const step = (2 * Math.PI) / shapes.length; for (let i = 0; i < shapes.length; i++) { const angle = step * i - Math.PI / 2; shapes[i].x = cx + radius * Math.cos(angle) - shapes[i].width / 2; shapes[i].y = cy + radius * Math.sin(angle) - shapes[i].height / 2; } } // ── Match Size ── export function matchWidth(shapes: ShapeEl[]) { if (shapes.length < 2) return; const maxW = Math.max(...shapes.map((s) => s.width)); for (const s of shapes) s.width = maxW; } export function matchHeight(shapes: ShapeEl[]) { if (shapes.length < 2) return; const maxH = Math.max(...shapes.map((s) => s.height)); for (const s of shapes) s.height = maxH; } export function matchSize(shapes: ShapeEl[]) { matchWidth(shapes); matchHeight(shapes); } // ── Registry: kebab-name → function ── const TRANSFORM_MAP: Record void> = { "align-left": alignLeft, "align-right": alignRight, "align-center-h": alignCenterH, "align-top": alignTop, "align-bottom": alignBottom, "align-center-v": alignCenterV, "distribute-h": distributeH, "distribute-v": distributeV, "arrange-row": arrangeRow, "arrange-column": arrangeColumn, "arrange-grid": arrangeGrid, "arrange-circle": arrangeCircle, "match-width": matchWidth, "match-height": matchHeight, "match-size": matchSize, }; /** Install the transform map on window so the action executor can find them. */ export function installSelectionTransforms() { (window as any).__miSelectionTransforms = TRANSFORM_MAP; }