Files
aida_front/src/component/Canvas/CanvasEditor/commands/CutSelectionToNewLayerCommand.js
2025-09-24 16:26:40 +08:00

842 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
createLayer,
findInChildLayers,
LayerType,
} from "../utils/layerHelper.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand, Command } from "./Command.js";
import { CreateImageLayerCommand } from "./LayerCommands.js";
import { fabric } from "fabric-with-all";
import { generateId } from "../utils/helper.js";
import { ClearSelectionCommand } from "./LassoCutoutCommand.js";
import { ClearSelectionContentCommand } from "./ClearSelectionContentCommand.js";
import i18n from "@/lang/index.ts";
const { t } = i18n.global;
/**
* 剪切选区到新图层命令
* 实现将选区内容复制到新图层,并将复制的选区作为组的遮罩
*/
export class CutSelectionToNewLayerCommand extends CompositeCommand {
constructor(options = {}) {
super([], {
name: "套索抠图",
description: "将选区抠图到新图层",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.selectionManager = options.selectionManager;
this.toolManager = options.toolManager;
this.sourceLayerId = options.sourceLayerId;
this.newLayerName = options.newLayerName || "抠图";
this.newLayerId = null;
this.cutoutImageUrl = null;
this.fabricImage = null;
this.executedCommands = [];
// 高清截图选项
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数
this.groupId = options.groupId || generateId("lasso-copy-group-");
this.groupName = options.groupName || t(`Canvas.ConstituencyGroup`);
this.groupLayer = null; // 新增:保存组图层的引用
this.originalLayersLength = 0; // 新增:保存原始图层数量
this.clippingMaskId = generateId("clipping-mask-");
// 在初始化时克隆保存选区对象,避免撤销后重做时获取不到选区对象
this._clonedSelectionObject = null;
this._initializeClonedSelection();
}
/**
* 初始化克隆的选区对象
* @private
*/
async _initializeClonedSelection() {
if (this.selectionManager) {
const selectionObject = this.selectionManager.getSelectionObject();
if (selectionObject) {
try {
this._clonedSelectionObject = await this._cloneObject(
selectionObject
);
console.log("套索抠图:选区对象已克隆保存");
} catch (error) {
console.error("套索抠图:克隆选区对象失败:", error);
// 备用方案:序列化保存
this.serializedSelectionObject = selectionObject.toObject([
"id",
"layerId",
"layerName",
"parentId",
]);
}
}
}
}
async execute() {
if (!this.canvas || !this.layerManager || !this.selectionManager) {
console.error("无法执行套索抠图:参数无效");
return false;
}
try {
this.executedCommands = [];
// 保存原始图层数量,用于撤销时的验证
this.originalLayersLength = this.layerManager.layers.value.length;
// 获取选区:优先使用克隆的选区对象
const selectionObject = await this._getSelectionObject();
if (!selectionObject) {
console.error("无法执行套索抠图:当前没有选区");
return false;
}
// 确定源图层
const sourceLayer = this.layerManager.getActiveLayer();
if (!sourceLayer) {
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);
// 使用createRasterizedImage执行抠图操作
// this.cutoutImageUrl = await this._performCutoutWithRasterized(
// sourceObjects,
// selectionObject,
// selectionBounds
// );
// if (!this.cutoutImageUrl) {
// console.error("抠图失败");
// return false;
// }
this.fabricImage = await this._performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
);
// // 创建fabric图像对象传递选区边界信息
// this.fabricImage = await this._createFabricImage(
// this.cutoutImageUrl,
// selectionBounds
// );
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 result = await createImageLayerCmd.execute();
this.newLayerId = createImageLayerCmd.newLayerId;
this.executedCommands.push(createImageLayerCmd);
// 2. 清除选区命令
const clearSelectionCmd = new ClearSelectionCommand({
canvas: this.canvas,
selectionManager: this.selectionManager,
});
// 执行清除选区命令
await clearSelectionCmd.execute();
this.executedCommands.push(clearSelectionCmd);
const topLayerIndex = this.layerManager.layers.value.findIndex(
(layer) => layer.id === this.newLayerId
);
const selectLayer = this.layerManager.layers.value[topLayerIndex];
// 创建新的组图层
this.groupLayer = createLayer({
id: this.groupId,
name: this.groupName || t(`Canvas.ConstituencyGroup`),
type: LayerType.GROUP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
children: [],
});
this.fabricImage.set({
selectable: true,
evented: true,
});
selectLayer.parentId = this.groupId; // 设置新图层的parentId为组图层ID
selectLayer.fabricObjects = [
this.fabricImage.toObject("id", "layerId", "layerName", "parentId"),
];
this.groupLayer.clippingMask = clippingMask.toObject(["id", "layerId"]); // 设置组图层的fabricObject为遮罩图像
this.groupLayer.children.push(selectLayer);
selectionObject.set({
id: generateId("selectionObject-"),
customType: "selectionObject",
});
this.groupLayer.selectObject = selectionObject.toObject([
"id",
"customType",
]);
// 插入新组图层
this.layerManager.layers.value.splice(topLayerIndex, 1, this.groupLayer);
this.canvas.discardActiveObject();
this.canvas.setActiveObject(this.fabricImage);
await this.layerManager.updateLayersObjectsInteractivity(true);
console.log(`套索抠图完成新图层ID: ${this.newLayerId}`);
return {
newLayerId: this.newLayerId,
cutoutImageUrl: this.cutoutImageUrl,
guroupId: this.groupId,
groupName: this.groupName,
};
} catch (error) {
console.error("套索抠图过程中出错:", error);
// 如果已经创建了新图层,需要进行清理
if (this.newLayerId) {
try {
await this.layerManager.removeLayer(this.newLayerId);
} catch (cleanupError) {
console.warn("清理新图层失败:", cleanupError);
}
}
// 清理组图层(如果已创建)
if (this.groupLayer && this.groupId) {
try {
const groupIndex = this.layerManager.layers.value.findIndex(
(layer) => layer.id === this.groupId
);
if (groupIndex !== -1) {
this.layerManager.layers.value.splice(groupIndex, 1);
console.log(`清理了异常创建的组图层: ${this.groupId}`);
}
this.groupLayer = null;
} catch (cleanupError) {
console.warn("清理组图层失败:", cleanupError);
}
}
// 尝试回滚已执行的命令
if (this.executedCommands.length > 0) {
try {
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
const command = this.executedCommands[i];
if (command && typeof command.undo === "function") {
await command.undo();
}
}
this.executedCommands = [];
} catch (rollbackError) {
console.warn("回滚已执行命令失败:", rollbackError);
}
}
throw error;
}
}
async undo() {
try {
console.log(`↩️ 开始撤销套索抠图操作`);
// 1. 首先移除组图层(如果存在)
if (this.groupId) {
const groupIndex = this.layerManager.layers.value.findIndex(
(layer) => layer.id === this.groupId
);
if (groupIndex !== -1) {
console.log(`↩️ 移除组图层: ${this.groupId}`);
// 从图层列表中移除组图层
this.layerManager.layers.value.splice(groupIndex, 1);
}
}
if (this.fabricImage) {
console.log(`↩️ 移除抠图图像: ${this.fabricImage.id}`);
// 从画布中移除抠图图像
this.canvas.remove(this.fabricImage);
}
// 2. 逆序撤销所有已执行的子命令
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
const command = this.executedCommands[i];
if (command && typeof command.undo === "function") {
try {
console.log(`↩️ 撤销子命令: ${command.constructor.name}`);
await command.undo();
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
// 子命令撤销失败不中断整个撤销过程
}
}
}
// 3. 清理状态
this.executedCommands = [];
this.newLayerId = null;
this.cutoutImageUrl = null;
this.fabricImage = null;
this.groupLayer = null; // 清理组图层引用
// 注意不重置groupId因为重做时可能需要使用相同的ID
// 4. 更新画布和图层交互性
await this.layerManager.updateLayersObjectsInteractivity(true);
console.log(`✅ 套索抠图撤销完成`);
return true;
} catch (error) {
console.error("❌ 撤销套索抠图失败:", error);
return false;
}
}
/**
* 获取命令信息
* @returns {Object} 命令详细信息
*/
getInfo() {
return {
name: this.name,
description: this.description,
newLayerId: this.newLayerId,
newLayerName: this.newLayerName,
groupId: this.groupId,
groupName: this.groupName,
executedCommandsCount: this.executedCommands.length,
hasGroupLayer: !!this.groupLayer,
sourceLayerId: this.sourceLayerId,
highResolutionEnabled: this.highResolutionEnabled,
baseResolutionScale: this.baseResolutionScale,
hasSerializedSelection: !!this.serializedSelectionObject,
selectionType: this.serializedSelectionObject?.type || null,
subCommands: this.executedCommands.map((cmd) => ({
name: cmd.constructor.name,
info: cmd.getInfo ? cmd.getInfo() : {},
})),
};
}
/**
* 获取图层的所有对象(包括子图层,从画布中查找真实对象)
* @param {Object} layer 图层对象
* @returns {Array} 真实的fabric对象数组
* @private
*/
_getLayerObjects(layer) {
const objects = [];
const canvasObjects = this.canvas.getObjects();
// 递归获取图层及其子图层的所有对象
const collectLayerObjects = (currentLayer) => {
// 处理图层的fabricObjects
if (
currentLayer.fabricObjects &&
Array.isArray(currentLayer.fabricObjects)
) {
currentLayer.fabricObjects.forEach((fabricRef) => {
if (fabricRef && fabricRef.id) {
// 从画布中查找真实的对象
const realObject = canvasObjects.find(
(obj) => obj.id === fabricRef.id
);
if (realObject && realObject.visible) {
objects.push(realObject);
}
}
});
}
// 处理单个fabricObject背景图层等
if (currentLayer.fabricObject && currentLayer.fabricObject.id) {
const realObject = canvasObjects.find(
(obj) => obj.id === currentLayer.fabricObject.id
);
if (realObject && realObject.visible) {
objects.push(realObject);
}
}
// 递归处理子图层
if (currentLayer.children && Array.isArray(currentLayer.children)) {
currentLayer.children.forEach((childLayer) => {
if (childLayer.visible !== false) {
// 只处理可见的子图层
collectLayerObjects(childLayer);
}
});
}
};
collectLayerObjects(layer);
console.log(`从图层 "${layer.name}" 收集到 ${objects.length} 个可见对象`);
return objects;
}
/**
* 使用createRasterizedImage执行抠图操作
* @param {Array} sourceObjects 源对象数组
* @param {Object} selectionObject 选区对象
* @param {Object} selectionBounds 选区边界
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
) {
try {
console.log("=== 开始使用createRasterizedImage执行抠图 ===");
console.log(`源对象数量: ${sourceObjects.length}`);
console.log(`选区边界:`, selectionBounds);
// 确定缩放因子,确保高质量输出
let scaleFactor = this.baseResolutionScale;
if (this.highResolutionEnabled) {
const devicePixelRatio = window.devicePixelRatio || 1;
scaleFactor = Math.max(scaleFactor, devicePixelRatio * 1.5);
}
// 使用createRasterizedImage生成栅格化图像将选区作为裁剪路径
const rasterizedDataURL = await createRasterizedImage({
canvas: this.canvas,
fabricObjects: sourceObjects,
clipPath: selectionObject, // 使用选区作为裁剪路径
trimWhitespace: true,
trimPadding: 2,
quality: 1.0,
format: "png",
scaleFactor: scaleFactor,
// isReturenDataURL: true, // 返回DataURL
preserveOriginalQuality: true, // 启用高质量模式
selectionManager: this.selectionManager, // 传递选区管理器,用于获取羽化值
});
if (!rasterizedDataURL) {
throw new Error("栅格化生成图像失败");
}
console.log(`✅ 高质量抠图完成,缩放因子: ${scaleFactor}x`);
return rasterizedDataURL;
} catch (error) {
console.error("使用createRasterizedImage执行抠图失败:", error);
// 如果createRasterizedImage失败回退到原始方法
console.log("⚠️ 回退到原始抠图方法...");
return await this._performCutout(
{ fabricObjects: sourceObjects },
selectionObject
);
}
}
/**
* 原始抠图方法(作为备用方案)
* @param {Object} sourceLayer 源图层
* @param {Object} selectionObject 选区对象
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutout(sourceLayer, selectionObject) {
try {
console.log("=== 使用原始方法执行抠图 ===");
// 获取选区边界
const selectionBounds = selectionObject.getBoundingRect(true, true);
// 保存画布当前状态
const originalActiveObject = this.canvas.getActiveObject();
const originalSelection = this.canvas.selection;
// 临时禁用画布选择
this.canvas.selection = false;
this.canvas.discardActiveObject();
let tempGroup = null;
let originalObjects = [];
try {
// 收集源图层中的可见对象
const visibleObjects = sourceLayer.fabricObjects.filter(
(obj) => obj.visible
);
if (visibleObjects.length === 0) {
throw new Error("源图层没有可见对象");
}
// 如果只有一个对象且已经是组,直接使用
if (visibleObjects.length === 1 && visibleObjects[0].type === "group") {
tempGroup = visibleObjects[0];
} else {
// 创建临时组
const clonedObjects = [];
for (const obj of visibleObjects) {
const cloned = await this._cloneObject(obj);
clonedObjects.push(cloned);
}
tempGroup = new fabric.Group(clonedObjects, {
selectable: false,
evented: false,
});
this.canvas.add(tempGroup);
}
// 设置选区为裁剪路径
const clipPath = await this._cloneObject(selectionObject);
clipPath.set({
fill: "",
stroke: "",
absolutePositioned: true,
originX: "left",
originY: "top",
});
tempGroup.set({
clipPath: clipPath,
});
this.canvas.renderAll();
// 计算渲染区域
const renderBounds = {
left: selectionBounds.left,
top: selectionBounds.top,
width: selectionBounds.width,
height: selectionBounds.height,
};
// 设置高分辨率倍数
let highResolutionScale = 1;
if (this.highResolutionEnabled) {
const devicePixelRatio = window.devicePixelRatio || 1;
highResolutionScale = Math.max(
this.baseResolutionScale,
devicePixelRatio * 2
);
}
// 创建用于导出的高分辨率canvas
const exportCanvas = document.createElement("canvas");
const exportCtx = exportCanvas.getContext("2d", {
alpha: true,
willReadFrequently: false,
colorSpace: "srgb",
});
const actualWidth = Math.round(
renderBounds.width * highResolutionScale
);
const actualHeight = Math.round(
renderBounds.height * highResolutionScale
);
exportCanvas.width = actualWidth;
exportCanvas.height = actualHeight;
exportCanvas.style.width = renderBounds.width + "px";
exportCanvas.style.height = renderBounds.height + "px";
exportCtx.imageSmoothingEnabled = true;
exportCtx.imageSmoothingQuality = "high";
exportCtx.clearRect(0, 0, actualWidth, actualHeight);
const vpt = this.canvas.viewportTransform;
const zoom = this.canvas.getZoom();
exportCtx.save();
exportCtx.scale(highResolutionScale, highResolutionScale);
exportCtx.translate(-renderBounds.left, -renderBounds.top);
if (zoom !== 1 || vpt[4] !== 0 || vpt[5] !== 0) {
exportCtx.transform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
}
tempGroup.render(exportCtx);
exportCtx.restore();
const dataUrl = exportCanvas.toDataURL("image/png", 1.0);
return dataUrl;
} finally {
// 清理和恢复
if (tempGroup) {
tempGroup.set({ clipPath: null });
if (originalObjects.length > 0) {
this.canvas.remove(tempGroup);
}
}
this.canvas.selection = originalSelection;
if (originalActiveObject) {
this.canvas.setActiveObject(originalActiveObject);
}
this.canvas.renderAll();
}
} catch (error) {
console.error("原始方法执行抠图失败:", error);
return null;
}
}
/**
* 从DataURL创建fabric图像对象
* @param {String} dataUrl 图像DataURL
* @param {Object} selectionBounds 选区边界信息
* @returns {fabric.Image} fabric图像对象
* @private
*/
async _createFabricImage(dataUrl, selectionBounds) {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(
dataUrl,
(img) => {
if (!img) {
reject(new Error("无法从DataURL创建图像"));
return;
}
// 使用选区的位置作为图像位置
let targetLeft = selectionBounds.left + selectionBounds.width / 2;
let targetTop = selectionBounds.top + selectionBounds.height / 2;
// 设置图像属性,保持选区的原始尺寸
img.set({
left: targetLeft,
top: targetTop,
originX: "center",
originY: "center",
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
cornerStyle: "circle",
cornerColor: "#007aff",
cornerSize: 10,
transparentCorners: false,
borderColor: "#007aff",
borderScaleFactor: 2,
// 优化图像渲染质量
objectCaching: false, // 禁用缓存以确保最佳质量
statefullCache: true,
noScaleCache: false,
});
// 更新坐标
img.setCoords();
console.log(
`图像创建完成,位置: (${targetLeft}, ${targetTop}), 尺寸: ${img.width}x${img.height}`
);
resolve(img);
},
{
crossOrigin: "anonymous",
// 确保图像以最高质量加载
quality: 1.0,
}
);
});
}
/**
* 克隆fabric对象
* @param {Object} obj fabric对象
* @returns {Object} 克隆的对象
* @private
*/
async _cloneObject(obj) {
return new Promise((resolve, reject) => {
if (!obj) {
reject(new Error("对象无效,无法克隆"));
return;
}
try {
obj.clone((cloned) => {
if (cloned) {
resolve(cloned);
} else {
reject(new Error("对象克隆失败"));
}
});
} catch (error) {
reject(error);
}
});
}
/**
* 序列化选区对象
* @private
*/
_serializeSelectionObject() {
try {
if (!this.selectionManager) {
console.warn("选区管理器不存在,无法序列化选区对象");
return;
}
const selectionObject = this.selectionManager.getSelectionObject();
if (!selectionObject) {
console.warn("当前没有选区对象,无法序列化");
return;
}
// 将选区对象转换为可序列化的对象
this.serializedSelectionObject = selectionObject.toObject([
"id",
"layerId",
"layerName",
"parentId",
]);
console.log("选区对象已序列化保存");
} catch (error) {
console.error("序列化选区对象失败:", error);
this.serializedSelectionObject = null;
}
}
/**
* 反序列化选区对象
* @returns {Promise<Object>} 选区对象
* @private
*/
async _getSelectionObject() {
try {
// 优先使用克隆的选区对象
if (this._clonedSelectionObject) {
console.log("使用克隆的选区对象");
return await this._cloneObject(this._clonedSelectionObject);
}
// 尝试从选区管理器获取当前选区
const currentSelection = this.selectionManager.getSelectionObject();
if (currentSelection) {
console.log("从选区管理器获取到当前选区");
return currentSelection;
}
// 最后使用序列化数据恢复(备用方案)
if (!this.serializedSelectionObject) {
console.error("没有可用的选区对象数据");
return null;
}
console.log("从序列化数据恢复选区对象");
// 根据选区对象类型进行反序列化
return new Promise((resolve, reject) => {
const objectType = this.serializedSelectionObject.type;
if (objectType === "path") {
fabric.Path.fromObject(this.serializedSelectionObject, (path) => {
if (path) {
console.log("路径选区对象反序列化成功");
resolve(path);
} else {
reject(new Error("路径选区对象反序列化失败"));
}
});
} else if (objectType === "polygon") {
fabric.Polygon.fromObject(
this.serializedSelectionObject,
(polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
}
);
} else if (objectType === "rect") {
fabric.Rect.fromObject(this.serializedSelectionObject, (rect) => {
if (rect) {
console.log("矩形选区对象反序列化成功");
resolve(rect);
} else {
reject(new Error("矩形选区对象反序列化失败"));
}
});
} else if (objectType === "ellipse" || objectType === "circle") {
fabric.Ellipse.fromObject(
this.serializedSelectionObject,
(ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
}
);
} else {
// 通用对象反序列化
fabric.util.enlivenObjects(
[this.serializedSelectionObject],
(objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
}
);
}
});
} catch (error) {
console.error("获取选区对象失败:", error);
return null;
}
}
}