This commit is contained in:
Feng 2025-04-26 10:07:13 +05:30 committed by GitHub
commit e33b90a513
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 395 additions and 1 deletions

View 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;
}
}

View 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 };

View file

@ -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