diff --git a/src/disitrubte.ts b/src/disitrubte.ts index 0766e3e2..4865c9dd 100644 --- a/src/disitrubte.ts +++ b/src/disitrubte.ts @@ -7,6 +7,10 @@ interface Box { minY: number; maxX: number; maxY: number; + midX: number; + midY: number; + width: number; + height: number; } export interface Distribution { @@ -18,22 +22,62 @@ export const distributeElements = ( selectedElements: ExcalidrawElement[], distribution: Distribution, ): ExcalidrawElement[] => { - const start = distribution.axis === "x" ? "minX" : "minY"; - const extent = distribution.axis === "x" ? "width" : "height"; - - const selectionBoundingBox = getCommonBoundingBox(selectedElements); + const [start, mid, end, extent] = + distribution.axis === "x" + ? (["minX", "midX", "maxX", "width"] as const) + : (["minY", "midY", "maxY", "height"] as const); + const bounds = getCommonBoundingBox(selectedElements); const groups = getMaximumGroups(selectedElements) .map((group) => [group, getCommonBoundingBox(group)] as const) - .sort((a, b) => a[1][start] - b[1][start]); + .sort((a, b) => a[1][mid] - b[1][mid]); let span = 0; for (const group of groups) { span += group[1][extent]; } - const step = (selectionBoundingBox[extent] - span) / (groups.length - 1); - let pos = selectionBoundingBox[start]; + const step = (bounds[extent] - span) / (groups.length - 1); + + if (step < 0) { + // If we have a negative step, we'll need to distribute from centers + // rather than from gaps. Buckle up, this is a weird one. + + // Get indices of boxes that define start and end of our bounding box + const i0 = groups.findIndex((g) => g[1][start] === bounds[start]); + const i1 = groups.findIndex((g) => g[1][end] === bounds[end]); + + // Get our step, based on the distance between the center points of our + // start and end boxes + const step = + (groups[i1][1][mid] - groups[i0][1][mid]) / (groups.length - 1); + + let pos = groups[i0][1][mid]; + + return groups.flatMap(([group, box], i) => { + const translation = { + x: 0, + y: 0, + }; + + // Don't move our start and end boxes + if (i !== i0 && i !== i1) { + pos += step; + translation[distribution.axis] = pos - box[mid]; + } + + return group.map((element) => + newElementWith(element, { + x: element.x + translation.x, + y: element.y + translation.y, + }), + ); + }); + } + + // Distribute from gaps + + let pos = bounds[start]; return groups.flatMap(([group, box]) => { const translation = { @@ -41,17 +85,15 @@ export const distributeElements = ( y: 0, }; - if (Math.abs(pos - box[start]) >= 1e-6) { - translation[distribution.axis] = pos - box[start]; - } + translation[distribution.axis] = pos - box[start]; - pos += box[extent]; pos += step; + pos += box[extent]; return group.map((element) => newElementWith(element, { - x: Math.round(element.x + translation.x), - y: Math.round(element.y + translation.y), + x: element.x + translation.x, + y: element.y + translation.y, }), ); }); @@ -79,9 +121,16 @@ export const getMaximumGroups = ( return Array.from(groups.values()); }; -const getCommonBoundingBox = ( - elements: ExcalidrawElement[], -): Box & { width: number; height: number } => { +const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => { const [minX, minY, maxX, maxY] = getCommonBounds(elements); - return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY }; + return { + minX, + minY, + maxX, + maxY, + width: maxX - minX, + height: maxY - minY, + midX: (minX + maxX) / 2, + midY: (minY + maxY) / 2, + }; };