mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge bf8f0c48c3
into 2a0d15799c
This commit is contained in:
commit
e33b90a513
15 changed files with 395 additions and 1 deletions
146
packages/element/src/arrange-algorithms/packer.ts
Normal file
146
packages/element/src/arrange-algorithms/packer.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
export interface Block {
|
||||
w: number;
|
||||
h: number;
|
||||
fit?: TreeNode | null;
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
used?: boolean;
|
||||
down?: TreeNode;
|
||||
right?: TreeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Growth packer based on https://github.com/jakesgordon/bin-packing/tree/master
|
||||
*
|
||||
* Added support to adding gaps to the packing by @feng94
|
||||
*/
|
||||
export class GrowingPacker {
|
||||
root: TreeNode | null;
|
||||
gap: number;
|
||||
|
||||
constructor(gap = 0) {
|
||||
this.root = null;
|
||||
this.gap = gap;
|
||||
}
|
||||
|
||||
fit(blocks: Block[]): void {
|
||||
const len = blocks.length;
|
||||
const w = len > 0 ? blocks[0].w : 0;
|
||||
const h = len > 0 ? blocks[0].h : 0;
|
||||
this.root = { x: 0, y: 0, w, h };
|
||||
|
||||
for (let n = 0; n < len; n++) {
|
||||
const block = blocks[n];
|
||||
const TreeNode = this.findNode(this.root, block.w, block.h);
|
||||
if (TreeNode) {
|
||||
block.fit = this.splitNode(TreeNode, block.w, block.h);
|
||||
} else {
|
||||
block.fit = this.growNode(block.w, block.h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private findNode(
|
||||
root: TreeNode | null,
|
||||
w: number,
|
||||
h: number,
|
||||
): TreeNode | null {
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
if (root.used) {
|
||||
return (
|
||||
this.findNode(root.right || null, w, h) ||
|
||||
this.findNode(root.down || null, w, h)
|
||||
);
|
||||
} else if (w <= root.w && h <= root.h) {
|
||||
return root;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private splitNode(TreeNode: TreeNode, w: number, h: number): TreeNode {
|
||||
TreeNode.used = true;
|
||||
TreeNode.down = {
|
||||
x: TreeNode.x,
|
||||
y: TreeNode.y + h + this.gap,
|
||||
w: TreeNode.w,
|
||||
h: TreeNode.h - h - this.gap,
|
||||
};
|
||||
TreeNode.right = {
|
||||
x: TreeNode.x + w + this.gap,
|
||||
y: TreeNode.y,
|
||||
w: TreeNode.w - w - this.gap,
|
||||
h,
|
||||
};
|
||||
return TreeNode;
|
||||
}
|
||||
|
||||
private growNode(w: number, h: number): TreeNode | null {
|
||||
if (!this.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canGrowDown = w <= this.root.w;
|
||||
const canGrowRight = h <= this.root.h;
|
||||
|
||||
const shouldGrowRight =
|
||||
canGrowRight && this.root.h >= this.root.w + w + this.gap;
|
||||
const shouldGrowDown =
|
||||
canGrowDown && this.root.w >= this.root.h + h + this.gap;
|
||||
|
||||
if (shouldGrowRight) {
|
||||
return this.growRight(w, h);
|
||||
} else if (shouldGrowDown) {
|
||||
return this.growDown(w, h);
|
||||
} else if (canGrowRight) {
|
||||
return this.growRight(w, h);
|
||||
} else if (canGrowDown) {
|
||||
return this.growDown(w, h);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private growRight(w: number, h: number): TreeNode | null {
|
||||
if (!this.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.root = {
|
||||
used: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: this.root.w + w + this.gap,
|
||||
h: this.root.h,
|
||||
down: this.root,
|
||||
right: { x: this.root.w + this.gap, y: 0, w, h: this.root.h },
|
||||
};
|
||||
|
||||
const TreeNode = this.findNode(this.root, w, h);
|
||||
return TreeNode ? this.splitNode(TreeNode, w, h) : null;
|
||||
}
|
||||
|
||||
private growDown(w: number, h: number): TreeNode | null {
|
||||
if (!this.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.root = {
|
||||
used: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: this.root.w,
|
||||
h: this.root.h + h + this.gap,
|
||||
down: { x: 0, y: this.root.h + this.gap, w: this.root.w, h },
|
||||
right: this.root,
|
||||
};
|
||||
|
||||
const TreeNode = this.findNode(this.root, w, h);
|
||||
return TreeNode ? this.splitNode(TreeNode, w, h) : null;
|
||||
}
|
||||
}
|
121
packages/element/src/arrange.ts
Normal file
121
packages/element/src/arrange.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { getCommonBoundingBox } from "./bounds";
|
||||
|
||||
import { getMaximumGroups } from "./groups";
|
||||
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
import { GrowingPacker, type Block } from "./arrange-algorithms/packer";
|
||||
|
||||
import type { BoundingBox } from "./bounds";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
ArrangeAlgorithms,
|
||||
} from "./types";
|
||||
|
||||
interface Group {
|
||||
group: ExcalidrawElement[];
|
||||
boundingBox: BoundingBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all elements relative to the group position
|
||||
*/
|
||||
const mutateGroup = (
|
||||
group: ExcalidrawElement[],
|
||||
update: { x: number; y: number },
|
||||
) => {
|
||||
// Determine the delta of the group position, vs the new update position
|
||||
const groupBoundingBox = getCommonBoundingBox(group);
|
||||
const deltaX = update.x - groupBoundingBox.minX;
|
||||
const deltaY = update.y - groupBoundingBox.minY;
|
||||
// Update the elements in the group
|
||||
|
||||
group.forEach((element) => {
|
||||
mutateElement(element, {
|
||||
x: element.x + deltaX,
|
||||
y: element.y + deltaY,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const arrangeElementsBinaryTreePacking = (
|
||||
groups: Group[],
|
||||
gap: number,
|
||||
): ExcalidrawElement[] => {
|
||||
const flattendGroups = groups.flatMap((g) => g.group);
|
||||
const commonBoundingBox = getCommonBoundingBox(flattendGroups);
|
||||
const origin = {
|
||||
x: commonBoundingBox.minX,
|
||||
y: commonBoundingBox.minY,
|
||||
};
|
||||
|
||||
const groupBlocks: (Block & {
|
||||
group: ExcalidrawElement[];
|
||||
})[] = groups
|
||||
// sort gropus by maxSide, highest to lowest
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.max(b.boundingBox.width, b.boundingBox.height) -
|
||||
Math.max(a.boundingBox.width, a.boundingBox.height),
|
||||
)
|
||||
.map((g) => ({
|
||||
w: g.boundingBox.width,
|
||||
h: g.boundingBox.height,
|
||||
group: g.group,
|
||||
}));
|
||||
|
||||
const packer = new GrowingPacker(gap);
|
||||
packer.fit(groupBlocks);
|
||||
|
||||
const groupsAdded = [];
|
||||
for (let n = 0; n < groupBlocks.length; n++) {
|
||||
const block = groupBlocks[n];
|
||||
if (block.fit) {
|
||||
// Add to elements translation
|
||||
groupsAdded.push(block);
|
||||
// DrawRectangle(block.fit.x, block.fit.y, block.w, block.h);
|
||||
}
|
||||
}
|
||||
// For each groupsAdded, we need to actually perform the translation
|
||||
// and update the elements
|
||||
groupsAdded.forEach((group) => {
|
||||
mutateGroup(group.group, {
|
||||
x: origin.x + (group.fit?.x ?? 0),
|
||||
y: origin.y + (group.fit?.y ?? 0),
|
||||
});
|
||||
});
|
||||
|
||||
return groupsAdded.flatMap((group) => group.group);
|
||||
};
|
||||
|
||||
const arrangeElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
algorithm: ArrangeAlgorithms,
|
||||
gap: number,
|
||||
): ExcalidrawElement[] => {
|
||||
// Determine the groups that we would be rearranging, as we don't want to be
|
||||
// making any manipulations within groups
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
);
|
||||
const groupBoundingBoxes = groups.map((group) => ({
|
||||
group,
|
||||
boundingBox: getCommonBoundingBox(group),
|
||||
}));
|
||||
|
||||
switch (algorithm) {
|
||||
case "bin-packing":
|
||||
return arrangeElementsBinaryTreePacking(groupBoundingBoxes, gap);
|
||||
default:
|
||||
console.warn(
|
||||
`Unimplemented algorithm [${algorithm}] - using bin-packing`,
|
||||
);
|
||||
return arrangeElementsBinaryTreePacking(groupBoundingBoxes, gap);
|
||||
}
|
||||
};
|
||||
|
||||
export { arrangeElements };
|
|
@ -412,3 +412,5 @@ export type NonDeletedSceneElementsMap = Map<
|
|||
export type ElementsMapOrArray =
|
||||
| readonly ExcalidrawElement[]
|
||||
| Readonly<ElementsMap>;
|
||||
|
||||
export type ArrangeAlgorithms = "bin-packing"; // Add more here if required
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue