align.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { ExcalidrawElement } from "./element/types";
  2. import { newElementWith } from "./element/mutateElement";
  3. import { getCommonBounds } from "./element";
  4. interface Box {
  5. minX: number;
  6. minY: number;
  7. maxX: number;
  8. maxY: number;
  9. }
  10. export interface Alignment {
  11. position: "start" | "center" | "end";
  12. axis: "x" | "y";
  13. }
  14. export const alignElements = (
  15. selectedElements: ExcalidrawElement[],
  16. alignment: Alignment,
  17. ): ExcalidrawElement[] => {
  18. const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
  19. const selectionBoundingBox = getCommonBoundingBox(selectedElements);
  20. return groups.flatMap((group) => {
  21. const translation = calculateTranslation(
  22. group,
  23. selectionBoundingBox,
  24. alignment,
  25. );
  26. return group.map((element) =>
  27. newElementWith(element, {
  28. x: element.x + translation.x,
  29. y: element.y + translation.y,
  30. }),
  31. );
  32. });
  33. };
  34. export const getMaximumGroups = (
  35. elements: ExcalidrawElement[],
  36. ): ExcalidrawElement[][] => {
  37. const groups: Map<String, ExcalidrawElement[]> = new Map<
  38. String,
  39. ExcalidrawElement[]
  40. >();
  41. elements.forEach((element: ExcalidrawElement) => {
  42. const groupId =
  43. element.groupIds.length === 0
  44. ? element.id
  45. : element.groupIds[element.groupIds.length - 1];
  46. const currentGroupMembers = groups.get(groupId) || [];
  47. groups.set(groupId, [...currentGroupMembers, element]);
  48. });
  49. return Array.from(groups.values());
  50. };
  51. const calculateTranslation = (
  52. group: ExcalidrawElement[],
  53. selectionBoundingBox: Box,
  54. { axis, position }: Alignment,
  55. ): { x: number; y: number } => {
  56. const groupBoundingBox = getCommonBoundingBox(group);
  57. const [min, max]: ["minX" | "minY", "maxX" | "maxY"] =
  58. axis === "x" ? ["minX", "maxX"] : ["minY", "maxY"];
  59. const noTranslation = { x: 0, y: 0 };
  60. if (position === "start") {
  61. return {
  62. ...noTranslation,
  63. [axis]: selectionBoundingBox[min] - groupBoundingBox[min],
  64. };
  65. } else if (position === "end") {
  66. return {
  67. ...noTranslation,
  68. [axis]: selectionBoundingBox[max] - groupBoundingBox[max],
  69. };
  70. } // else if (position === "center") {
  71. return {
  72. ...noTranslation,
  73. [axis]:
  74. (selectionBoundingBox[min] + selectionBoundingBox[max]) / 2 -
  75. (groupBoundingBox[min] + groupBoundingBox[max]) / 2,
  76. };
  77. };
  78. const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
  79. const [minX, minY, maxX, maxY] = getCommonBounds(elements);
  80. return { minX, minY, maxX, maxY };
  81. };