feat: 优化选区功能,修复部分bug

This commit is contained in:
bighuixiang
2025-07-10 01:01:46 +08:00
parent 943b49c1d7
commit 7359fe2f9a
11 changed files with 1667 additions and 126 deletions

View File

@@ -35,11 +35,13 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数
this.groupId = options.groupId || `cutout-group-${Date.now()}`;
this.groupId = options.groupId || generateId("lasso-copy-group-");
this.groupName = options.groupName || `选区组`;
this.groupLayer = null; // 新增:保存组图层的引用
this.originalLayersLength = 0; // 新增:保存原始图层数量
this.clippingMaskId = generateId("clipping-mask-");
// 在初始化时克隆保存选区对象,避免撤销后重做时获取不到选区对象
this._clonedSelectionObject = null;
this._initializeClonedSelection();
@@ -105,6 +107,16 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
return false;
}
const clippingMask = fabric.util.object.clone(selectionObject);
clippingMask.set({
id: this.clippingMaskId,
selectable: false,
evented: false,
hasControls: false,
// layerId: this.groupId,
visible: true,
});
// 获取选区边界信息用于后续定位
const selectionBounds = selectionObject.getBoundingRect(true, true);
@@ -185,12 +197,8 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
selectLayer.fabricObjects = [
this.fabricImage.toObject("id", "layerId", "layerName", "parentId"),
];
this.groupLayer.clippingMask = this.fabricImage.toObject(
"id",
"layerId",
"layerName",
"parentId"
); // 设置组图层的fabricObject为遮罩图像
this.groupLayer.clippingMask = clippingMask.toObject(["id", "layerId"]); // 设置组图层的fabricObject为遮罩图像
this.groupLayer.children.push(selectLayer);
// 插入新组图层

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ import { generateId } from "../utils/helper.js";
/**
* 套索抠图命令
* 实现将选区内容抠图到新图层的功能
* 实现将选区内容到新图层遮罩
*/
export class LassoCutoutCommand extends CompositeCommand {
constructor(options = {}) {
@@ -36,8 +36,11 @@ export class LassoCutoutCommand extends CompositeCommand {
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数
this.groupId = options.groupId || `cutout-group-${Date.now()}`;
this.groupId = options.groupId || generateId("lasso-group-");
this.groupName = options.groupName || `选区组`;
this.clippingMaskId = generateId("clipping-mask-");
this.groupLayer = null; // 新增:保存组图层的引用
this.originalLayersLength = 0; // 新增:保存原始图层数量
@@ -51,6 +54,8 @@ export class LassoCutoutCommand extends CompositeCommand {
this.originalFabricObjects = []; // 保存原图层的所有fabric对象序列化
this.originalCanvasObjects = []; // 保存从画布中获取的真实fabric对象
this._initializeOriginalLayerInfo();
this.oldActiveLayerId = this.layerManager.activeLayerId.value; // 保存旧的活动图层ID
}
/**
@@ -144,14 +149,24 @@ export class LassoCutoutCommand extends CompositeCommand {
}
// 获取源图层的所有对象(包括子图层)
const sourceObjects = this._getLayerObjects(sourceLayer);
if (sourceObjects.length === 0) {
console.error("无法执行套索抠图:源图层没有可见对象");
return false;
}
// const sourceObjects = this._getLayerObjects(sourceLayer);
// if (sourceObjects.length === 0) {
// console.error("无法执行套索抠图:源图层没有可见对象");
// return false;
// }
const clippingMask = fabric.util.object.clone(selectionObject);
clippingMask.set({
id: this.clippingMaskId,
selectable: false,
evented: false,
hasControls: false,
// layerId: this.groupId,
visible: true,
});
// 获取选区边界信息用于后续定位
const selectionBounds = selectionObject.getBoundingRect(true, true);
// const selectionBounds = selectionObject.getBoundingRect(true, true);
// 使用createRasterizedImage执行抠图操作
// this.cutoutImageUrl = await this._performCutoutWithRasterized(
@@ -164,46 +179,34 @@ export class LassoCutoutCommand extends CompositeCommand {
// return false;
// }
this.fabricImage = await this._performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
);
// this.fabricImage = await this._performCutoutWithRasterized(
// sourceObjects,
// selectionObject,
// selectionBounds
// );
// // 创建fabric图像对象传递选区边界信息
// this.fabricImage = await this._createFabricImage(
// this.cutoutImageUrl,
// selectionBounds
// );
if (!this.fabricImage) {
console.error("创建图像对象失败");
return false;
}
// if (!this.fabricImage) {
// console.error("创建图像对象失败");
// return false;
// }
// 1. 创建图像图层命令
const createImageLayerCmd = new CreateImageLayerCommand({
layerManager: this.layerManager,
fabricImage: this.fabricImage,
toolManager: this.toolManager,
layerName: this.newLayerName,
});
// const createImageLayerCmd = new CreateImageLayerCommand({
// layerManager: this.layerManager,
// fabricImage: this.fabricImage,
// toolManager: this.toolManager,
// layerName: this.newLayerName,
// });
// 执行创建图像图层命令
const result = await createImageLayerCmd.execute();
this.newLayerId = createImageLayerCmd.newLayerId;
this.executedCommands.push(createImageLayerCmd);
// 2. 删除原图层命令
const removeOriginalLayerCmd = new RemoveLayerCommand({
canvas: this.canvas,
layers: this.layerManager.layers,
layerId: this.originalLayer.id,
activeLayerId: this.layerManager.activeLayerId,
});
// 执行删除原图层命令
await removeOriginalLayerCmd.execute();
this.executedCommands.push(removeOriginalLayerCmd);
// // 执行创建图像图层命令
// const result = await createImageLayerCmd.execute();
// this.newLayerId = createImageLayerCmd.newLayerId;
// this.executedCommands.push(createImageLayerCmd);
// 3. 清除选区命令
const clearSelectionCmd = new ClearSelectionCommand({
@@ -216,10 +219,10 @@ export class LassoCutoutCommand extends CompositeCommand {
this.executedCommands.push(clearSelectionCmd);
const topLayerIndex = this.layerManager.layers.value.findIndex(
(layer) => layer.id === this.newLayerId
(layer) => layer.id === this.originalLayer.id
);
const selectLayer = this.layerManager.layers.value[topLayerIndex];
// const selectLayer = this.layerManager.layers.value[topLayerIndex];
// 创建新的组图层
this.groupLayer = createLayer({
@@ -233,28 +236,47 @@ export class LassoCutoutCommand extends CompositeCommand {
children: [],
});
this.fabricImage.set({
selectable: true,
evented: true,
// this.fabricImage.set({
// selectable: true,
// evented: true,
// });
const selectLayer = createLayer({
name: `选区空图层`,
type: LayerType.EMPTY,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
children: [],
});
selectLayer.parentId = this.groupId; // 设置新图层的parentId为组图层ID
selectLayer.fabricObjects = [
this.fabricImage.toObject("id", "layerId", "layerName", "parentId"),
];
this.groupLayer.clippingMask = this.fabricImage.toObject(
"id",
"layerId",
"layerName",
"parentId"
); // 设置组图层的fabricObject为遮罩图像
// selectLayer.fabricObjects = [
// 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,
});
// 执行删除原图层命令
await removeOriginalLayerCmd.execute();
this.executedCommands.push(removeOriginalLayerCmd);
this.groupLayer.clippingMask = clippingMask.toObject(["id", "layerId"]); // 设置组图层的fabricObject为遮罩图像
this.groupLayer.children.push(selectLayer);
// 插入新组图层
this.layerManager.layers.value.splice(topLayerIndex, 1, this.groupLayer);
this.layerManager.layers.value.splice(topLayerIndex, 0, this.groupLayer);
this.layerManager.activeLayerId.value = selectLayer.id; // 设置新组图层为活动图层
this.canvas.discardActiveObject();
this.canvas.setActiveObject(this.fabricImage);
// this.canvas.setActiveObject(this.fabricImage);
await this.layerManager.updateLayersObjectsInteractivity(true);
console.log(`套索抠图完成新图层ID: ${this.newLayerId}`);
@@ -328,6 +350,8 @@ export class LassoCutoutCommand extends CompositeCommand {
}
}
this.layerManager.activeLayerId.value = this.oldActiveLayerId; // 恢复旧的活动图层ID
if (this.fabricImage) {
console.log(`↩️ 移除抠图图像: ${this.fabricImage.id}`);
// 从画布中移除抠图图像

View File

@@ -14,6 +14,7 @@ import {
} from "../utils/helper";
import { createRasterizedImage } from "../utils/rasterizedImage";
import { message } from "ant-design-vue";
import { restoreFabricObject } from "../utils/objectHelper";
/**
* 栅格化图层命令
@@ -82,7 +83,7 @@ export class RasterizeLayerCommand extends Command {
this.canvas.renderAll();
// 检查是否有遮罩对象
const maskObject = this._getMaskObject();
const maskObject = await this._getMaskObject();
// 创建栅格化图像
const rasterizedImage = await createRasterizedImage({
@@ -392,9 +393,9 @@ export class RasterizeLayerCommand extends Command {
* @returns {Object|null} 遮罩对象或null
* @private
*/
_getMaskObject() {
async _getMaskObject() {
// 如果图层有clippingMask获取对应的fabric对象
if (this.layer?.clippingMask?.id) {
if (this.layer?.clippingMask) {
const { object: maskObject } = findObjectById(
this.canvas,
this.layer.clippingMask.id
@@ -405,6 +406,7 @@ export class RasterizeLayerCommand extends Command {
);
return maskObject;
}
return await restoreFabricObject(this.layer.clippingMask);
}
console.log("📎 未找到遮罩对象");

View File

@@ -601,8 +601,8 @@ function handleLayerClick(layer, event) {
layer.children.length > 0
) {
// 如果是组图层,设置第一个子图层为活动图层
setActiveLayer(layer.children[0].id, { parentId: layer.id });
layerManager?.setAllActiveGroupLayerCanvasObject?.(layer);
setActiveLayer(layer.children[0].id, { parentId: layer.id });
} else {
// 否则直接设置当前图层为活动图层
setActiveLayer(layer.id);

View File

@@ -39,7 +39,6 @@ import {
loadImageUrlToLayer,
loadImage,
} from "./utils/imageHelper.js";
import { next } from "lodash-es";
// import MinimapPanel from "./components/MinimapPanel.vue";
const KeyboardShortcutHelp = defineAsyncComponent(() =>
import("./components/KeyboardShortcutHelp.vue")

View File

@@ -407,8 +407,8 @@ export class CanvasManager {
// 居中所有画布元素,包括背景层和其他元素
this.centerAllObjects();
// 重新渲染画布使变更生效
this.canvas.renderAll();
// // 重新渲染画布使变更生效
// this.canvas.renderAll();
}
/**
@@ -510,7 +510,7 @@ export class CanvasManager {
}
// 重新渲染画布
this.canvas.renderAll();
// this.canvas.renderAll();
}
/**
@@ -958,6 +958,9 @@ export class CanvasManager {
// 解析JSON字符串
try {
const parsedJson = JSON.parse(json);
this.canvasWidth.value = parsedJson.canvasWidth || this.width;
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
return new Promise(async (resolve, reject) => {
const tempLayers = JSON.parse(parsedJson?.layers) || [];
@@ -974,6 +977,7 @@ export class CanvasManager {
}
this.layers.value = tempLayers;
debugger;
// this.canvasWidth.value = parsedJson.canvasWidth || this.width;
// this.canvasHeight.value = parsedJson.canvasHeight || this.height;

View File

@@ -67,6 +67,8 @@ import {
} from "../utils/helper";
import { message } from "ant-design-vue";
import { fabric } from "fabric-with-all";
import { getOriginObjectInfo } from "../utils/layerUtils";
import { restoreFabricObject } from "../utils/objectHelper";
/**
* 图层管理器 - 负责管理画布上的所有图层
@@ -333,52 +335,94 @@ export class LayerManager {
});
// 设置裁剪对象
layers.forEach((layer) => {
layers.forEach(async (layer) => {
if (layer.clippingMask) {
const activeObject = this.canvas.getActiveObject();
if (activeObject?._objects?.length > 1) {
console.log(activeObject?._objects?.length);
return false; // 如果是多选对象,则不设置裁剪路径
}
// 反序列化 clippingMask
const clippingMaskFabricObject = await restoreFabricObject(
layer.clippingMask,
this.canvas
);
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
// 设置绝对定位
// ...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) => {
const { object } = findObjectById(
this.canvas,
layer.clippingMask?.id
);
if (object) {
const tempClipPath = fabric.util.object.clone(object);
tempClipPath.clipPath = null;
tempClipPath.set({
// 设置绝对定位
// ...layer.clippingMask, // 恢复原定位
absolutePositioned: true,
});
if (clippingMaskFabricObject) {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
if (childObj) {
childObj.clipPath = tempClipPath;
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 {
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, // 恢复原定位
absolutePositioned: true,
});
obj.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;
// // }
// }
}
});
}
@@ -932,17 +976,16 @@ export class LayerManager {
// 设置激活当前图层下画布中的所有对象,并变成选择组
setAllActiveGroupLayerCanvasObject(layer) {
// 获取当前图层下所有元素
let layerMask = null;
// 选择当前组下所有画布元素
const allObjects = layer.children.reduce((acc, child) => {
// 如果子图层有fabricObjects则添加到结果数组
child?.fabricObjects?.forEach((obj) => {
const { object } = findObjectById(this.canvas, obj.id);
if (object) {
if (!layerMask) {
layerMask = fabric.util.object.clone(object.clipPath);
}
object.clipPath = null; // 确保克隆的遮罩没有clipPath
// if (!layerMask) {
// layerMask = fabric.util.object.clone(object.clipPath);
// }
// object.clipPath = null; // 确保克隆的遮罩没有clipPath
acc.push(object);
}
});
@@ -950,10 +993,10 @@ export class LayerManager {
if (child?.fabricObject) {
const { object } = findObjectById(this.canvas, child?.fabricObject.id);
if (object) {
if (!layerMask) {
layerMask = fabric.util.object.clone(object.clipPath);
}
object.clipPath = null; // 确保克隆的遮罩没有clipPath
// if (!layerMask) {
// layerMask = fabric.util.object.clone(object.clipPath);
// }
// object.clipPath = null; // 确保克隆的遮罩没有clipPath
acc.push(object);
}
}
@@ -967,17 +1010,48 @@ export class LayerManager {
// 如果有对象,创建选择组
this.canvas.discardActiveObject(); // 取消当前活动对象
this.canvas.renderAll(); // 确保画布渲染
// const { object } = findObjectById(this.canvas, layer.clippingMask?.id);
// 选中多个对象,不是创建组
// 多个对象时创建活动选择组
const activeSelection = new fabric.ActiveSelection(allObjects, {
let activeSelection = new fabric.ActiveSelection(allObjects, {
canvas: this.canvas,
});
activeSelection.clipPath = layerMask; // 保留第一个对象的裁剪路径
// if (object) {
// const tempClipPath = fabric.util.object.clone(object);
// tempClipPath.clipPath = null;
// tempClipPath.set({
// // 设置绝对定位
// // ...layer.clippingMask, // 恢复原定位
// ...getOriginObjectInfo(layer.clippingMask),
// absolutePositioned: true,
// });
// activeSelection.clipPath = tempClipPath; // 保留第一个对象的裁剪路径
// }
// // 监听选择取消事件,恢复原始裁剪路径
// const restoreClipPaths = () => {
// allObjects.forEach((obj) => {
// if (obj._originalClipPath !== undefined) {
// obj.clipPath = obj._originalClipPath;
// delete obj._originalClipPath;
// }
// });
// this.canvas.off("selection:cleared", restoreClipPaths);
// this.canvas.off("selection:updated", restoreClipPaths);
// };
// this.canvas.on("selection:cleared", restoreClipPaths);
// this.canvas.on("selection:updated", restoreClipPaths);
// 设置活动选择组的属性
this.canvas.setActiveObject(activeSelection);
this.canvas.renderAll();
activeSelection = null; // 清理引用,避免内存泄漏
// 确保选择组正确渲染
// activeSelection.setCoords();
}
}

View File

@@ -1,3 +1,5 @@
import { generateId } from "./helper";
/**
* 图层类型枚举
*/

View File

@@ -20,11 +20,13 @@ export function buildLayerAssociations(layer, canvasObjects) {
}
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组
// clippingMask 可能是一个fabricObject或组 也可能是一个简单对象
const clippingMaskObj = canvasObjects.find(
(obj) => obj.id === layer.clippingMask.id
);
layer.clippingMask = clippingMaskObj?.toObject?.(["id"]) || null;
layer.clippingMask = clippingMaskObj
? clippingMaskObj?.toObject?.(["id"])
: layer.clippingMask;
}
// 处理多个fabricObjects关联
@@ -170,12 +172,15 @@ export function simplifyLayers(layers) {
opacity: layer.opacity,
isBackground: layer.isBackground || false,
isFixed: layer.isFixed || false,
clippingMask: layer.clippingMask
? {
id: layer.clippingMask.id,
type: layer.clippingMask.type,
}
: null,
clippingMask:
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
layer.clippingMask ||
null, // 可能是一个fabricObject或组 也可能是一个简单对象
// ? {
// id: layer.clippingMask.id,
// type: layer.clippingMask.type,
// }
// : null,
fabricObject: layer.fabricObject
? {
id: layer.fabricObject.id,
@@ -226,11 +231,21 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
const restoredLayer = { ...layer };
// 恢复clippingMask关联
if (layer.clippingMask?.id) {
// if (layer.clippingMask?.id) {
// const clippingMaskObj = canvasObjects.find(
// (obj) => obj.id === layer.clippingMask.id
// );
// restoredLayer.clippingMask = clippingMaskObj || null;
// }
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组 也可能是一个简单对象
const clippingMaskObj = canvasObjects.find(
(obj) => obj.id === layer.clippingMask.id
);
restoredLayer.clippingMask = clippingMaskObj || null;
restoredLayer.clippingMask = clippingMaskObj
? clippingMaskObj?.toObject?.(["id"])
: layer.clippingMask;
}
// 恢复单个fabricObject关联
@@ -334,3 +349,30 @@ export function restoreFromSnapshot(snapshot, canvasObjects) {
return restoreLayers(snapshot.data, canvasObjects);
}
/**
* 获取对象的定位信息
* @param {Object} object 对象
* @returns {Object} 对象的定位信息
*/
export function getOriginObjectInfo(object) {
if (!object) {
console.warn("getOriginObjectInfo 请传入有效的fabric对象:", object);
return {};
}
// 获取对象的原始信息
const originInfo = {
left: object.left || 0,
top: object.top || 0,
scaleX: object.scaleX || 1,
scaleY: object.scaleY || 1,
angle: object.angle || 0,
flipX: object.flipX || false,
flipY: object.flipY || false,
skewX: object.skewX || 0,
skewY: object.skewY || 0,
};
return originInfo;
}

View File

@@ -0,0 +1,63 @@
import { fabric } from "fabric-with-all";
/**
* 将序列化对象恢复为 fabric 对象
* @param {Object} serializedObject - toObject() 生成的对象
* @param {fabric.Canvas} canvas - 目标画布
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
*/
export async function restoreFabricObject(serializedObject, canvas) {
return new Promise((resolve, reject) => {
const objectType = serializedObject.type;
// 定义恢复后的处理函数
const handleRestoredObject = (fabricObject) => {
if (!fabricObject) {
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
return;
}
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId)
fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName)
fabricObject.layerName = serializedObject.layerName;
// 更新坐标
fabricObject.setCoords();
// 添加到画布
// canvas.add(fabricObject);
resolve(fabricObject);
};
// 根据类型选择恢复方法
switch (objectType) {
case "rect":
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
break;
case "circle":
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
break;
case "path":
fabric.Path.fromObject(serializedObject, handleRestoredObject);
break;
case "image":
fabric.Image.fromObject(serializedObject, handleRestoredObject);
break;
case "group":
fabric.Group.fromObject(serializedObject, handleRestoredObject);
break;
default:
// 使用通用方法
fabric.util.enlivenObjects([serializedObject], (objects) => {
if (objects && objects[0]) {
handleRestoredObject(objects[0]);
} else {
reject(new Error("对象恢复失败"));
}
});
}
});
}