2020-11-23 18:16:23 +00:00
|
|
|
import { ExcalidrawElement } from "./element/types";
|
|
|
|
import { newElementWith } from "./element/mutateElement";
|
|
|
|
import { getCommonBounds } from "./element";
|
|
|
|
|
|
|
|
interface Box {
|
|
|
|
minX: number;
|
|
|
|
minY: number;
|
|
|
|
maxX: number;
|
|
|
|
maxY: number;
|
2020-11-25 23:20:56 +00:00
|
|
|
midX: number;
|
|
|
|
midY: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
2020-11-23 18:16:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface Distribution {
|
|
|
|
space: "between";
|
|
|
|
axis: "x" | "y";
|
|
|
|
}
|
|
|
|
|
|
|
|
export const distributeElements = (
|
|
|
|
selectedElements: ExcalidrawElement[],
|
|
|
|
distribution: Distribution,
|
|
|
|
): ExcalidrawElement[] => {
|
2020-11-25 23:20:56 +00:00
|
|
|
const [start, mid, end, extent] =
|
|
|
|
distribution.axis === "x"
|
|
|
|
? (["minX", "midX", "maxX", "width"] as const)
|
|
|
|
: (["minY", "midY", "maxY", "height"] as const);
|
2020-11-23 18:16:23 +00:00
|
|
|
|
2020-11-25 23:20:56 +00:00
|
|
|
const bounds = getCommonBoundingBox(selectedElements);
|
2020-11-23 18:16:23 +00:00
|
|
|
const groups = getMaximumGroups(selectedElements)
|
|
|
|
.map((group) => [group, getCommonBoundingBox(group)] as const)
|
2020-11-25 23:20:56 +00:00
|
|
|
.sort((a, b) => a[1][mid] - b[1][mid]);
|
2020-11-23 18:16:23 +00:00
|
|
|
|
|
|
|
let span = 0;
|
|
|
|
for (const group of groups) {
|
|
|
|
span += group[1][extent];
|
|
|
|
}
|
|
|
|
|
2020-11-25 23:20:56 +00:00
|
|
|
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
|
2020-11-29 18:32:51 +02:00
|
|
|
const index0 = groups.findIndex((g) => g[1][start] === bounds[start]);
|
|
|
|
const index1 = groups.findIndex((g) => g[1][end] === bounds[end]);
|
2020-11-25 23:20:56 +00:00
|
|
|
|
|
|
|
// Get our step, based on the distance between the center points of our
|
|
|
|
// start and end boxes
|
|
|
|
const step =
|
2020-11-29 18:32:51 +02:00
|
|
|
(groups[index1][1][mid] - groups[index0][1][mid]) / (groups.length - 1);
|
2020-11-25 23:20:56 +00:00
|
|
|
|
2020-11-29 18:32:51 +02:00
|
|
|
let pos = groups[index0][1][mid];
|
2020-11-25 23:20:56 +00:00
|
|
|
|
2020-11-29 18:32:51 +02:00
|
|
|
return groups.flatMap(([group, box], index) => {
|
2020-11-25 23:20:56 +00:00
|
|
|
const translation = {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Don't move our start and end boxes
|
2020-11-29 18:32:51 +02:00
|
|
|
if (index !== index0 && index !== index1) {
|
2020-11-25 23:20:56 +00:00
|
|
|
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];
|
2020-11-23 18:16:23 +00:00
|
|
|
|
|
|
|
return groups.flatMap(([group, box]) => {
|
|
|
|
const translation = {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
};
|
|
|
|
|
2020-11-25 23:20:56 +00:00
|
|
|
translation[distribution.axis] = pos - box[start];
|
2020-11-23 18:16:23 +00:00
|
|
|
|
|
|
|
pos += step;
|
2020-11-25 23:20:56 +00:00
|
|
|
pos += box[extent];
|
2020-11-23 18:16:23 +00:00
|
|
|
|
|
|
|
return group.map((element) =>
|
|
|
|
newElementWith(element, {
|
2020-11-25 23:20:56 +00:00
|
|
|
x: element.x + translation.x,
|
|
|
|
y: element.y + translation.y,
|
2020-11-23 18:16:23 +00:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getMaximumGroups = (
|
|
|
|
elements: ExcalidrawElement[],
|
|
|
|
): ExcalidrawElement[][] => {
|
|
|
|
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
|
|
|
String,
|
|
|
|
ExcalidrawElement[]
|
|
|
|
>();
|
|
|
|
|
|
|
|
elements.forEach((element: ExcalidrawElement) => {
|
|
|
|
const groupId =
|
|
|
|
element.groupIds.length === 0
|
|
|
|
? element.id
|
|
|
|
: element.groupIds[element.groupIds.length - 1];
|
|
|
|
|
|
|
|
const currentGroupMembers = groups.get(groupId) || [];
|
|
|
|
|
|
|
|
groups.set(groupId, [...currentGroupMembers, element]);
|
|
|
|
});
|
|
|
|
|
|
|
|
return Array.from(groups.values());
|
|
|
|
};
|
|
|
|
|
2020-11-25 23:20:56 +00:00
|
|
|
const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
|
2020-11-23 18:16:23 +00:00
|
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
2020-11-25 23:20:56 +00:00
|
|
|
return {
|
|
|
|
minX,
|
|
|
|
minY,
|
|
|
|
maxX,
|
|
|
|
maxY,
|
|
|
|
width: maxX - minX,
|
|
|
|
height: maxY - minY,
|
|
|
|
midX: (minX + maxX) / 2,
|
|
|
|
midY: (minY + maxY) / 2,
|
|
|
|
};
|
2020-11-23 18:16:23 +00:00
|
|
|
};
|