feat: 优化跨层级移动和套索抠图命令,支持异步执行,改进画布刷新逻辑,新增背景裁剪选项

This commit is contained in:
bighuixiang
2025-07-11 00:26:38 +08:00
parent d3e22f368b
commit 96e13cb22a
10 changed files with 129 additions and 121 deletions

View File

@@ -181,7 +181,7 @@ export class CrossLevelMoveCommand extends Command {
return ancestor ? checkChildren(ancestor.children) : false;
}
execute() {
async execute() {
console.log("🎯 执行跨层级移动命令:", {
layerId: this.layerId,
from: this.fromContainerType,
@@ -220,10 +220,13 @@ export class CrossLevelMoveCommand extends Command {
this.saveAfterState();
// 刷新画布
if (this.canvas) {
this.canvas.renderAll();
console.log("🎨 画布已刷新");
}
// if (this.canvas) {
// this.canvas.renderAll();
// console.log("🎨 画布已刷新");
// }
await this.layerManager?.updateLayersObjectsInteractivity();
this.canvas?.renderAll();
console.log("✅ 跨层级移动命令执行成功");
return true;
@@ -248,7 +251,7 @@ export class CrossLevelMoveCommand extends Command {
}
}
undo() {
async undo() {
if (!this.beforeState) {
throw new Error("没有保存的前置状态,无法撤销");
}
@@ -262,14 +265,10 @@ export class CrossLevelMoveCommand extends Command {
// 恢复fabric对象状态
this.restoreFabricObjectStates(this.beforeState.fabricObjects);
await this.layerManager?.updateLayersObjectsInteractivity();
this.canvas?.renderAll();
// 刷新画布
if (this.canvas) {
// 使用 requestAnimationFrame 确保DOM更新完成后再渲染
requestAnimationFrame(() => {
this.canvas.renderAll();
});
}
this.canvas?.renderAll();
console.log("✅ 跨层级移动命令撤销成功");
return true;
} catch (error) {
@@ -331,7 +330,7 @@ export class CrossLevelMoveCommand extends Command {
/**
* 重做命令
*/
redo() {
async redo() {
if (!this.afterState) {
throw new Error("没有保存的后置状态,无法重做");
}
@@ -345,13 +344,9 @@ export class CrossLevelMoveCommand extends Command {
// 恢复fabric对象状态
this.restoreFabricObjectStates(this.afterState.fabricObjects);
await this.layerManager?.updateLayersObjectsInteractivity();
// 刷新画布
if (this.canvas) {
// 使用 requestAnimationFrame 确保DOM更新完成后再渲染
requestAnimationFrame(() => {
this.canvas.renderAll();
});
}
this.canvas?.renderAll();
console.log("✅ 跨层级移动命令重做成功");
return true;

View File

@@ -2,6 +2,7 @@ import {
createLayer,
findInChildLayers,
LayerType,
OperationType,
} from "../utils/layerHelper.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand, Command } from "./Command.js";
@@ -11,6 +12,7 @@ import {
} from "./LayerCommands.js";
import { fabric } from "fabric-with-all";
import { generateId } from "../utils/helper.js";
import { ToolCommand } from "./ToolCommands.js";
/**
* 套索抠图命令
@@ -256,16 +258,16 @@ export class LassoCutoutCommand extends CompositeCommand {
// this.fabricImage.toObject("id", "layerId", "layerName", "parentId"),
// ];
// 2. 删除原图层命令
const removeOriginalLayerCmd = new RemoveLayerCommand({
canvas: this.canvas,
layers: this.layerManager.layers,
layerId: this.originalLayer.id,
activeLayerId: this.layerManager.activeLayerId,
});
// const removeOriginalLayerCmd = new RemoveLayerCommand({
// canvas: this.canvas,
// layers: this.layerManager.layers,
// layerId: this.originalLayer.id,
// activeLayerId: this.layerManager.activeLayerId,l
// });
// 执行删除原图层命令
await removeOriginalLayerCmd.execute();
this.executedCommands.push(removeOriginalLayerCmd);
// // 执行删除原图层命令
// await removeOriginalLayerCmd.execute();
// this.executedCommands.push(removeOriginalLayerCmd);
this.groupLayer.clippingMask = clippingMask.toObject(["id", "layerId"]); // 设置组图层的fabricObject为遮罩图像
@@ -275,6 +277,21 @@ export class LassoCutoutCommand extends CompositeCommand {
this.layerManager.activeLayerId.value = selectLayer.id; // 设置新组图层为活动图层
// 切换工具到选择模式
// 3. 切换工具到选择模式命令
if (this.toolManager) {
const toolCmd = new ToolCommand({
toolManager: this.toolManager,
tool: OperationType.SELECT,
previousTool: this.toolManager.getCurrentTool(),
});
// 执行工具切换命令
await toolCmd.execute();
this.commands.push(toolCmd);
this.executedCommands.push(toolCmd);
}
this.canvas.discardActiveObject();
// this.canvas.setActiveObject(this.fabricImage);
await this.layerManager.updateLayersObjectsInteractivity(true);

View File

@@ -2033,8 +2033,12 @@ export class LayerObjectsToGroupCommand extends Command {
const allObjects = [...originalObjects, ...newObjects];
const clipPath = allObjects?.[0]?.clipPath || null;
// 从画布中移除所有要组合的对象
allObjects.forEach((obj) => {
obj.clipPath = null;
obj.dirty = true; // 标记为脏对象
// obj.setCoords();
obj.opacity = 1;
removeCanvasObjectByObject(this.canvas, obj);
});
@@ -2052,6 +2056,10 @@ export class LayerObjectsToGroupCommand extends Command {
// 添加组对象到画布
this.canvas.add(groupObject);
groupObject.clipPath = clipPath; // 恢复剪切路径
groupObject.dirty = true; // 标记为脏对象
groupObject.setCoords();
// 更新图层的对象列表
// this.activeLayer.fabricObjects = [groupObject];
this.activeLayer.fabricObjects = [

View File

@@ -498,11 +498,13 @@ const canDeleteComputed = computed(() => {
.chosen {
opacity: 0.8;
transform: scale(1.02);
transform: scale(1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: 999;
background: linear-gradient(90deg, #fff2e8 0%, #ffd8bf 100%);
border: 2px solid #fa8c16;
border: 1px solid #fa8c16;
// border-left: none;
// border-right: none;
}
.drag {

View File

@@ -1348,9 +1348,10 @@ async function executeDirectMove(moveData) {
}
// 刷新画布渲染
if (layerManager?.canvas) {
layerManager.canvas.renderAll();
}
// if (layerManager?.canvas) {
// layerManager.canvas.renderAll();
// }
await layerManager?.updateLayersObjectsInteractivity?.();
// 清除选择状态
clearSelection();

View File

@@ -568,7 +568,7 @@
.ghost {
opacity: 0.4;
background-color: #fff7e6;
border: 2px dashed #faad14;
border: 1px dashed #faad14;
}
.chosen {

View File

@@ -783,6 +783,7 @@ defineExpose({
exportImage: ({
isContainBg = false, // 是否包含背景图层
isContainFixed = false, // 是否包含固定图层
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true则导出时裁剪到背景图层大小
layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组
expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG
@@ -790,6 +791,7 @@ defineExpose({
return canvasManager.exportImage({
isContainBg,
isContainFixed,
isCropByBg,
layerId,
layerIdArray,
expPicType,

View File

@@ -977,7 +977,6 @@ export class CanvasManager {
}
this.layers.value = tempLayers;
debugger;
// this.canvasWidth.value = parsedJson.canvasWidth || this.width;
// this.canvasHeight.value = parsedJson.canvasHeight || this.height;
@@ -1039,8 +1038,10 @@ export class CanvasManager {
// 使用LayerSort工具重新排列画布对象如果可用
await this?.layerManager?.layerSort?.rearrangeObjects();
this.layerManager.activeLayerId.value =
this.layers.value[0]?.id || parsedJson?.activeLayerId || null;
this.layerManager.activeLayerId.value = this.layers.value[0]
.children?.length
? this.layers.value[0].children[0].id
: this.layers.value[0]?.id || parsedJson?.activeLayerId || null;
// // 如果检测到红绿图模式内容,进行缩放调整
// if (this.enabledRedGreenMode) {

View File

@@ -28,6 +28,7 @@ export class ExportManager {
const {
isContainBg = false,
isContainFixed = false,
isCropByBg = false, // 是否使用背景大小裁剪
layerId = "",
layerIdArray = [],
expPicType = "png",
@@ -43,7 +44,8 @@ export class ExportManager {
layerId,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
restoreOpacityInRedGreen,
isCropByBg
);
}
@@ -52,10 +54,11 @@ export class ExportManager {
return this._exportMultipleLayers(
layerIdArray,
expPicType,
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
restoreOpacityInRedGreen,
isContainBg,
isCropByBg
);
}
@@ -65,7 +68,8 @@ export class ExportManager {
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
restoreOpacityInRedGreen,
isCropByBg
);
} catch (error) {
console.error("导出图片失败:", error);
@@ -143,7 +147,8 @@ export class ExportManager {
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
restoreOpacityInRedGreen,
isCropByBg
) {
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
@@ -193,7 +198,8 @@ export class ExportManager {
isContainBg,
isContainFixed,
isRedGreenMode,
restoreOpacityInRedGreen
restoreOpacityInRedGreen,
isCropByBg
) {
// 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder(
@@ -215,13 +221,38 @@ export class ExportManager {
restoreOpacityInRedGreen
);
}
let canvasClipPath = this.canvas.clipPath;
if (isCropByBg) {
const cropWidth =
this.canvasManager?.canvasWidth?.value ||
this.canvas?.canvasWidth ||
this.canvas.width;
const cropHeight =
this.canvasManager?.canvasHeight?.value ||
this.canvas?.canvasHeight ||
this.canvas.height;
canvasClipPath = new fabric.Rect({
left: this.canvas.width / 2,
top: this.canvas.height / 2,
width: cropWidth,
height: cropHeight,
originX: "center",
originY: "center",
fill: "#fff",
stroke: "transparent",
strokeWidth: 0,
});
canvasClipPath.set({
absolutePositioned: true,
});
canvasClipPath.setCoords();
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen,
this.canvas.clipPath
canvasClipPath
);
}

View File

@@ -336,9 +336,10 @@ export class LayerManager {
// 设置裁剪对象
layers.forEach(async (layer) => {
let clippingMaskFabricObject = null;
if (layer.clippingMask) {
// 反序列化 clippingMask
const clippingMaskFabricObject = await restoreFabricObject(
clippingMaskFabricObject = await restoreFabricObject(
layer.clippingMask,
this.canvas
);
@@ -349,80 +350,30 @@ export class LayerManager {
// ...getOriginObjectInfo(layer.clippingMask), // 恢复原定位
absolutePositioned: true,
});
// const activeObject = this.canvas.getActiveObject();
// if (activeObject?._objects?.length > 1) {
// const { object } = findObjectById(
// this.canvas,
// layer.clippingMask?.id
// );
// if (!object) return;
// const tempClipPath = fabric.util.object.clone(object);
// tempClipPath.clipPath = null;
// tempClipPath.set({
// // 设置绝对定位
// ...getOriginObjectInfo(layer.clippingMask), // 恢复原定位
// absolutePositioned: true,
// });
// activeObject.clipPath = tempClipPath;
// // 确保选择组正确渲染
// // activeObject.setCoords();
// console.log(activeObject?._objects?.length);
// return; // 如果是多选对象,则不设置裁剪路径
// }
// 如果是组图层 则给所有子对象设置裁剪对象
if (layer.type === LayerType.GROUP || layer.children?.length > 0) {
layer.children.forEach((childLayer) => {
if (clippingMaskFabricObject) {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
if (childObj) {
childObj.clipPath = clippingMaskFabricObject;
}
}
// const { object } = findObjectById(
// this.canvas,
// layer.clippingMask?.id
// );
// if (object) {
// const tempClipPath = fabric.util.object.clone(object);
// tempClipPath.clipPath = null;
// tempClipPath.set({
// // 设置绝对定位
// // ...layer.clippingMask, // 恢复原定位
// ...getOriginObjectInfo(layer.clippingMask),
// absolutePositioned: true,
// });
// const childObj = this.canvas
// .getObjects()
// .find((o) => o.layerId === childLayer.id);
// if (childObj) {
// childObj.clipPath = tempClipPath;
// }
// }
});
} else if (clippingMaskFabricObject) {
obj.clipPath = clippingMaskFabricObject;
}
// {
// // const { object } = findObjectById(
// // this.canvas,
// // layer.clippingMask?.id
// // );
// // if (object) {
// // const tempClipPath = fabric.util.object.clone(object);
// // tempClipPath.clipPath = null; // 确保克隆的遮罩没有clipPath
// // tempClipPath.set({
// // // 设置绝对定位
// // // ...layer.clippingMask, // 恢复原定位
// // ...getOriginObjectInfo(layer.clippingMask),
// // absolutePositioned: true,
// // });
// // obj.clipPath = tempClipPath;
// // }
// }
}
// 如果是组图层 则给所有子对象设置裁剪对象
if (layer.type === LayerType.GROUP || layer.children?.length > 0) {
layer.children.forEach((childLayer) => {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
if (childObj) {
childObj.clipPath = clippingMaskFabricObject;
childObj.dirty = true; // 标记为脏对象
childObj.setCoords();
}
});
} else {
layer.fabricObjects?.forEach((obj) => {
const fabricObject = this.canvas
.getObjects()
.find((o) => o.id === obj.id);
if (fabricObject) {
fabricObject.clipPath = clippingMaskFabricObject;
fabricObject.dirty = true; // 标记为脏对象
fabricObject.setCoords();
}
});
}
});
}