Skip to content

Commit 8489ebb

Browse files
committed
feat: 添加图片批量插入时的矩形平铺布局功能
在StageStore中新增了图片批量插入时的矩形平铺布局功能,确保图片在二维空间均匀分布。通过CommonUtils中的packRectangles方法实现矩形布局,并支持自定义间距。同时,优化了图片插入流程,确保图片插入时的位置和间距符合预期。
1 parent 3bd98fb commit 8489ebb

File tree

7 files changed

+169
-15
lines changed

7 files changed

+169
-15
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "0.0.2",
55
"type": "module",
66
"scripts": {
7-
"format": "prettier --write .",
7+
"format": "prettier --write ./src",
88
"dev": "vite",
99
"build": "vite build",
1010
"preview": "vite preview",

src/modules/stage/StageStore.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { ElementActionTypes, ElementsActionParam, ElementActionCallback } from "
3434
import GlobalConfig from "@/config";
3535
import { TaskQueue } from "@/modules/render/RenderQueue";
3636
import { QueueTask } from "../render/RenderTask";
37+
import { ImageMargin } from "@/types/Stage";
3738

3839
/**
3940
* 调整组件层级
@@ -2211,11 +2212,13 @@ export default class StageStore implements IStageStore {
22112212
*
22122213
* @param image
22132214
* @param options
2215+
* @param position
2216+
* @returns
22142217
*/
2215-
async createImageElementModel(image: HTMLImageElement | ImageData, options: Partial<ImageData>): Promise<ElementObject> {
2218+
async createImageElementModel(image: HTMLImageElement | ImageData, options: Partial<ImageData>, position?: IPoint): Promise<ElementObject> {
22162219
const { colorSpace } = options;
22172220
const { width, height } = image;
2218-
const coords = CommonUtils.getBoxByCenter(GlobalConfig.stageParams.worldCoord, {
2221+
const coords = CommonUtils.getBoxByCenter(position || GlobalConfig.stageParams.worldCoord, {
22192222
width,
22202223
height,
22212224
});
@@ -2245,16 +2248,12 @@ export default class StageStore implements IStageStore {
22452248
* 插入图片
22462249
*
22472250
* @param image
2251+
* @param position
22482252
* @returns
22492253
*/
2250-
async _insertImage(image: HTMLImageElement | ImageData): Promise<IElement> {
2251-
let colorSpace;
2252-
if (image instanceof ImageData) {
2253-
colorSpace = image.colorSpace;
2254-
image = ImageUtils.createImageFromImageData(image);
2255-
await ImageUtils.waitForImageLoad(image);
2256-
}
2257-
const model = await this.createImageElementModel(image, { colorSpace: colorSpace });
2254+
async _insertImage(image: (HTMLImageElement & { colorSpace: PredefinedColorSpace }), position: IPoint): Promise<IElement> {
2255+
let colorSpace = image.colorSpace;
2256+
const model = await this.createImageElementModel(image, { colorSpace: colorSpace }, position);
22582257
return this.insertAfterElementByModel(model);
22592258
}
22602259

@@ -2266,12 +2265,14 @@ export default class StageStore implements IStageStore {
22662265
*/
22672266
async insertImageElements(images: (HTMLImageElement[] | ImageData[])): Promise<IElement[]> {
22682267
const result: IElement[] = [];
2268+
images = await ImageUtils.convertImages(images);
2269+
const placed = CommonUtils.packRectangles(images.map(image => ({ width: image.width, height: image.height })), GlobalConfig.stageParams.worldCoord, ImageMargin);
22692270
await new Promise((resolve) => {
22702271
let taskQueue = new TaskQueue();
2271-
images.forEach((imageData, index) => {
2272+
images.forEach((img, index) => {
22722273
taskQueue.add(
22732274
new QueueTask(async () => {
2274-
const element = await this._insertImage(imageData);
2275+
const element = await this._insertImage(img as (HTMLImageElement & { colorSpace: PredefinedColorSpace }), placed[index]);
22752276
result.push(element);
22762277
if (index === images.length - 1) {
22772278
resolve(null);

src/types/IStageStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export default interface IStageStore extends IStageSetter {
150150
// 刷新组件
151151
refreshElements(elements: IElement[]): void;
152152
// 创建图片组件的数据模型
153-
createImageElementModel(image: HTMLImageElement | ImageData, options: Partial<ImageData>): Promise<ElementObject>;
153+
createImageElementModel(image: HTMLImageElement | ImageData, options: Partial<ImageData>, position?: IPoint): Promise<ElementObject>;
154154
// 插入图片组件
155155
insertImageElements(images: (HTMLImageElement[] | ImageData[])): Promise<IElement[]>;
156156
// 插入文本组件

src/types/Stage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ export enum CursorTypes {
2525

2626
// 舞台自动缩放时的内边距
2727
export const AutoFitPadding = 100;
28+
29+
// 图片批量生成时,各图片之间的间距
30+
export const ImageMargin = 20;

src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export type IPoint = {
1010
y: number;
1111
};
1212

13+
// 矩形
14+
export type IRect = IPoint & ISize;
15+
1316
// 3D坐标
1417
export type IPoint3D = IPoint & {
1518
z: number;

src/utils/CommonUtils.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IPoint, ISize } from "@/types";
1+
import { IPoint, IRect, ISize } from "@/types";
22
import { nanoid } from "nanoid";
33
import MathUtils from "@/utils/MathUtils";
44
import { isNumber } from "lodash";
@@ -515,4 +515,133 @@ export default class CommonUtils {
515515
height,
516516
};
517517
}
518+
519+
/**
520+
* 二维平铺算法, 确保矩形在二维空间均匀分布
521+
*
522+
* 1. 每行的高度不需要相同
523+
* 2. 每行的个数不需要相同
524+
* 3. 第一行在中间,第二行在第一行的上方,第三行在第一行的下方,呈上下上下的方式进行布局
525+
* 4. 每行按照左对齐的方式进行布局
526+
* 5. 每行的矩形之间的间距为margin
527+
*
528+
* @param rectangles 矩形数组
529+
* @param center 中心点
530+
* @param margin 矩形之间的间距
531+
* @returns 矩形数组
532+
*/
533+
static packRectangles(rectangles: ISize[], center: IPoint, margin: number = 0): IRect[] {
534+
if (rectangles.length === 0) return [];
535+
// 利用np加减枝算法,计算第一行的最大宽度
536+
let rowSize = Math.ceil(Math.sqrt(rectangles.length));
537+
if (rowSize >= 1) {
538+
rowSize--;
539+
}
540+
let rowWidth = 0;
541+
while (true) {
542+
rowSize++;
543+
rowWidth = rectangles.slice(0, rowSize).reduce((prev, curr) => prev + curr.width, 0) + (rowSize - 1) * margin;
544+
if (rowSize === rectangles.length) {
545+
break;
546+
}
547+
const widths: number[] = [];
548+
for (let i = 0; i < rectangles.length; i += rowSize) {
549+
const width = rectangles.slice(i, i + rowSize).reduce((prev, curr) => prev + curr.width, 0) + (rowSize - 1) * margin;
550+
widths.push(width);
551+
i += rowSize;
552+
}
553+
let gtCounter = 0;
554+
for (let i = 0; i < widths.length; i++) {
555+
if (widths[i] <= rowWidth) {
556+
gtCounter++;
557+
}
558+
}
559+
if (gtCounter >= Math.ceil(widths.length / 2)) {
560+
break;
561+
}
562+
}
563+
const result: IRect[][] = [];
564+
let totalHeight = 0;
565+
let currentRowWidth = 0;
566+
let currentRowHeight = 0;
567+
// 计算第一行的矩形
568+
const x = center.x - rowWidth / 2;
569+
const y = center.y - rectangles[0].height / 2;
570+
let row: IRect[] = [];
571+
for (let i = 0; i < rowSize; i++) {
572+
const { width, height } = rectangles[i];
573+
row.push({
574+
x: x + currentRowWidth + (currentRowWidth === 0 ? 0: margin) + width / 2,
575+
y: y + height / 2,
576+
width,
577+
height,
578+
});
579+
if (height > currentRowHeight) {
580+
currentRowHeight = height;
581+
}
582+
currentRowWidth += width + (currentRowWidth === 0? 0: margin);
583+
}
584+
result.push(row);
585+
586+
if (rowSize < rectangles.length) {
587+
totalHeight += currentRowHeight;
588+
currentRowWidth = 0;
589+
currentRowHeight = 0;
590+
row = [];
591+
let currentRowSize = 0;
592+
let currentRowDirection = 1; // 1: 向上, -1: 向下
593+
let lastY = y + totalHeight;
594+
let lastUpperRowIndex: number = -1;
595+
596+
// 重置当前行的状态
597+
function reset(): void {
598+
currentRowDirection = -currentRowDirection;
599+
totalHeight += currentRowHeight + margin;
600+
currentRowWidth = 0;
601+
currentRowHeight = 0;
602+
currentRowSize = 0;
603+
row = [];
604+
}
605+
606+
// 计算其他行的矩形,以第一行的第一个矩形为基准,左对齐
607+
for (let i = rowSize; i < rectangles.length; i++) {
608+
const { width, height } = rectangles[i];
609+
row.push({
610+
x: x + currentRowWidth + (currentRowWidth === 0 ? 0: margin) + width / 2,
611+
y: 0,
612+
width,
613+
height,
614+
});
615+
if (height > currentRowHeight) {
616+
currentRowHeight = height;
617+
}
618+
currentRowSize++;
619+
currentRowWidth += width + (currentRowWidth === 0? 0: margin);
620+
if (i < rectangles.length - 1 && currentRowWidth + rectangles[i + 1].width + margin > rowWidth || i === rectangles.length - 1) {
621+
result.push(row);
622+
// 计算当前行的y坐标
623+
if (currentRowDirection === 1) {
624+
lastY = lastY - totalHeight - margin - currentRowHeight;
625+
lastUpperRowIndex = result.length - 1;
626+
} else {
627+
lastY = lastY + totalHeight + margin + currentRowHeight;
628+
}
629+
row.forEach((item, index) => {
630+
item.y = lastY + (currentRowDirection === -1? -currentRowHeight: 0) + row[index].height / 2;
631+
});
632+
reset();
633+
}
634+
}
635+
636+
if (lastUpperRowIndex !== -1) {
637+
const row = result[lastUpperRowIndex];
638+
const rowHeight = Math.max(...row.map(item => item.height));
639+
row.forEach((item) => {
640+
item.y = item.y + (rowHeight - item.height);
641+
});
642+
}
643+
}
644+
645+
return result.flat();
646+
}
518647
}

src/utils/ImageUtils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,22 @@ export default class ImageUtils {
4343
await this.waitForImageLoad(img);
4444
return img;
4545
}
46+
47+
/**
48+
* 转换图片
49+
*
50+
* @param images
51+
* @returns
52+
*/
53+
static async convertImages(images: (HTMLImageElement[] | ImageData[])): Promise<(HTMLImageElement & { colorSpace: PredefinedColorSpace })[]> {
54+
return Promise.all(images.map(async (image) => {
55+
if (image instanceof ImageData) {
56+
const colorSpace = image.colorSpace;
57+
image = ImageUtils.createImageFromImageData(image);
58+
image.colorSpace = colorSpace;
59+
await ImageUtils.waitForImageLoad(image);
60+
}
61+
return image;
62+
}));
63+
}
4664
}

0 commit comments

Comments
 (0)