disitrubte.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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. midX: number;
  10. midY: number;
  11. width: number;
  12. height: number;
  13. }
  14. export interface Distribution {
  15. space: "between";
  16. axis: "x" | "y";
  17. }
  18. export const distributeElements = (
  19. selectedElements: ExcalidrawElement[],
  20. distribution: Distribution,
  21. ): ExcalidrawElement[] => {
  22. const [start, mid, end, extent] =
  23. distribution.axis === "x"
  24. ? (["minX", "midX", "maxX", "width"] as const)
  25. : (["minY", "midY", "maxY", "height"] as const);
  26. const bounds = getCommonBoundingBox(selectedElements);
  27. const groups = getMaximumGroups(selectedElements)
  28. .map((group) => [group, getCommonBoundingBox(group)] as const)
  29. .sort((a, b) => a[1][mid] - b[1][mid]);
  30. let span = 0;
  31. for (const group of groups) {
  32. span += group[1][extent];
  33. }
  34. const step = (bounds[extent] - span) / (groups.length - 1);
  35. if (step < 0) {
  36. // If we have a negative step, we'll need to distribute from centers
  37. // rather than from gaps. Buckle up, this is a weird one.
  38. // Get indices of boxes that define start and end of our bounding box
  39. const index0 = groups.findIndex((g) => g[1][start] === bounds[start]);
  40. const index1 = groups.findIndex((g) => g[1][end] === bounds[end]);
  41. // Get our step, based on the distance between the center points of our
  42. // start and end boxes
  43. const step =
  44. (groups[index1][1][mid] - groups[index0][1][mid]) / (groups.length - 1);
  45. let pos = groups[index0][1][mid];
  46. return groups.flatMap(([group, box], index) => {
  47. const translation = {
  48. x: 0,
  49. y: 0,
  50. };
  51. // Don't move our start and end boxes
  52. if (index !== index0 && index !== index1) {
  53. pos += step;
  54. translation[distribution.axis] = pos - box[mid];
  55. }
  56. return group.map((element) =>
  57. newElementWith(element, {
  58. x: element.x + translation.x,
  59. y: element.y + translation.y,
  60. }),
  61. );
  62. });
  63. }
  64. // Distribute from gaps
  65. let pos = bounds[start];
  66. return groups.flatMap(([group, box]) => {
  67. const translation = {
  68. x: 0,
  69. y: 0,
  70. };
  71. translation[distribution.axis] = pos - box[start];
  72. pos += step;
  73. pos += box[extent];
  74. return group.map((element) =>
  75. newElementWith(element, {
  76. x: element.x + translation.x,
  77. y: element.y + translation.y,
  78. }),
  79. );
  80. });
  81. };
  82. export const getMaximumGroups = (
  83. elements: ExcalidrawElement[],
  84. ): ExcalidrawElement[][] => {
  85. const groups: Map<String, ExcalidrawElement[]> = new Map<
  86. String,
  87. ExcalidrawElement[]
  88. >();
  89. elements.forEach((element: ExcalidrawElement) => {
  90. const groupId =
  91. element.groupIds.length === 0
  92. ? element.id
  93. : element.groupIds[element.groupIds.length - 1];
  94. const currentGroupMembers = groups.get(groupId) || [];
  95. groups.set(groupId, [...currentGroupMembers, element]);
  96. });
  97. return Array.from(groups.values());
  98. };
  99. const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
  100. const [minX, minY, maxX, maxY] = getCommonBounds(elements);
  101. return {
  102. minX,
  103. minY,
  104. maxX,
  105. maxY,
  106. width: maxX - minX,
  107. height: maxY - minY,
  108. midX: (minX + maxX) / 2,
  109. midY: (minY + maxY) / 2,
  110. };
  111. };