Files
aida_front/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js
X1627315083 c266967f16 接入画布
2025-06-09 10:25:54 +08:00

492 lines
15 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 { CompositeCommand } from "./Command.js";
import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js";
import { ToolCommand } from "./ToolCommands.js";
import { ClearSelectionCommand } from "./SelectionCommands.js";
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
//import { fabric } from "fabric-with-all";
/**
* 套索抠图命令
* 实现将选区内容抠图到新图层的功能
*/
export class LassoCutoutCommand 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 || 4; // 基础分辨率倍数提高到4倍获得更清晰的图像
}
async execute() {
if (!this.canvas || !this.layerManager || !this.selectionManager) {
console.error("无法执行套索抠图:参数无效");
return false;
}
try {
this.executedCommands = [];
// 获取选区
const selectionObject = this.selectionManager.getSelectionObject();
if (!selectionObject) {
console.error("无法执行套索抠图:当前没有选区");
return false;
}
// 确定源图层
const sourceLayer = this.layerManager.getActiveLayer();
if (!sourceLayer || sourceLayer.fabricObjects.length === 0) {
console.error("无法执行套索抠图:源图层无效");
return false;
}
// 获取选区边界信息用于后续定位
const selectionBounds = selectionObject.getBoundingRect(true, true);
// 执行在当前画布上的抠图操作
this.cutoutImageUrl = await this._performCutout(
sourceLayer,
selectionObject
);
if (!this.cutoutImageUrl) {
console.error("抠图失败");
return false;
}
// 创建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);
console.log(`套索抠图完成新图层ID: ${this.newLayerId}`);
return {
newLayerId: this.newLayerId,
cutoutImageUrl: this.cutoutImageUrl,
};
} catch (error) {
console.error("套索抠图过程中出错:", error);
// 如果已经创建了新图层,需要进行清理
if (this.newLayerId) {
try {
await this.layerManager.removeLayer(this.newLayerId);
} catch (cleanupError) {
console.warn("清理新图层失败:", cleanupError);
}
}
throw error;
}
}
async undo() {
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 = [];
this.newLayerId = null;
this.cutoutImageUrl = null;
this.fabricImage = null;
return true;
} catch (error) {
console.error("撤销套索抠图失败:", error);
return false;
}
}
/**
* 在当前画布上执行抠图操作
* @param {Object} sourceLayer 源图层
* @param {Object} selectionObject 选区对象
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutout(sourceLayer, selectionObject) {
try {
console.log("=== 开始在当前画布执行抠图 ===");
// 获取选区边界
const selectionBounds = selectionObject.getBoundingRect(true, true);
console.log(
`选区边界: left=${selectionBounds.left}, top=${selectionBounds.top}, width=${selectionBounds.width}, height=${selectionBounds.height}`
);
// 保存画布当前状态
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
);
console.log(`源图层可见对象数量: ${visibleObjects.length}`);
if (visibleObjects.length === 0) {
throw new Error("源图层没有可见对象");
}
// 如果只有一个对象且已经是组,直接使用
if (visibleObjects.length === 1 && visibleObjects[0].type === "group") {
tempGroup = visibleObjects[0];
console.log("使用现有组对象");
} else {
// 创建临时组
console.log("创建临时组...");
// 记录原始对象的位置和状态,用于后续恢复
originalObjects = visibleObjects.map((obj) => ({
object: obj,
originalLeft: obj.left,
originalTop: obj.top,
originalAngle: obj.angle,
originalScaleX: obj.scaleX,
originalScaleY: obj.scaleY,
}));
// 不需要从画布移除原对象,直接创建组
// 克隆对象来创建组,避免影响原对象
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
);
console.log(
`设备像素比: ${devicePixelRatio}, 基础倍数: ${this.baseResolutionScale}, 最终放大倍数: ${highResolutionScale}`
);
} else {
console.log("高分辨率渲染已禁用使用1x倍数");
}
// 创建用于导出的高分辨率canvas
const exportCanvas = document.createElement("canvas");
const exportCtx = exportCanvas.getContext("2d", {
alpha: true,
willReadFrequently: false,
colorSpace: "srgb",
});
// 设置canvas的实际像素尺寸放大倍数
const actualWidth = Math.round(
renderBounds.width * highResolutionScale
);
const actualHeight = Math.round(
renderBounds.height * highResolutionScale
);
exportCanvas.width = actualWidth;
exportCanvas.height = actualHeight;
// 设置canvas的显示尺寸CSS尺寸保持与选区一致
exportCanvas.style.width = renderBounds.width + "px";
exportCanvas.style.height = renderBounds.height + "px";
// 启用最高质量渲染设置
exportCtx.imageSmoothingEnabled = true;
exportCtx.imageSmoothingQuality = "high";
// 设置文本渲染质量
if (exportCtx.textRenderingOptimization) {
exportCtx.textRenderingOptimization = "optimizeQuality";
}
// 设置线条和图形的渲染质量
exportCtx.lineCap = "round";
exportCtx.lineJoin = "round";
exportCtx.miterLimit = 10;
// 设置画布背景为透明
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);
console.log(
`抠图完成,选区尺寸: ${renderBounds.width}x${renderBounds.height}, 实际渲染尺寸: ${actualWidth}x${actualHeight}, 放大倍数: ${highResolutionScale}x`
);
return dataUrl;
} finally {
// 清理和恢复
if (tempGroup) {
// 移除裁剪路径
tempGroup.set({ clipPath: null });
// 如果是我们创建的临时组,需要移除它
if (originalObjects.length > 0) {
this.canvas.remove(tempGroup);
// 恢复原始对象的状态(位置等信息保持不变)
originalObjects.forEach(
({
object,
originalLeft,
originalTop,
originalAngle,
originalScaleX,
originalScaleY,
}) => {
// 确保对象仍然在画布上且状态正确
if (!this.canvas.getObjects().includes(object)) {
this.canvas.add(object);
}
// 恢复原始变换状态(如果需要的话)
object.set({
left: originalLeft,
top: originalTop,
angle: originalAngle,
scaleX: originalScaleX,
scaleY: originalScaleY,
});
object.setCoords();
}
);
}
}
// 恢复画布状态
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;
}
// 计算画布中心位置
const canvasCenter = this.canvas.getCenter();
// 如果有选区边界信息,使用选区的原始位置和尺寸
let targetLeft = canvasCenter.left;
let targetTop = canvasCenter.top;
let targetWidth = img.width;
let targetHeight = img.height;
if (selectionBounds) {
// 使用选区的原始位置
targetLeft = selectionBounds.left + selectionBounds.width / 2;
targetTop = selectionBounds.top + selectionBounds.height / 2;
// 确保图像显示尺寸与选区尺寸一致
targetWidth = selectionBounds.width;
targetHeight = selectionBounds.height;
console.log(
`设置图像位置: left=${targetLeft}, top=${targetTop}, 尺寸: ${targetWidth}x${targetHeight}`
);
}
// 计算缩放比例以匹配目标尺寸
const scaleX = targetWidth / img.width;
const scaleY = targetHeight / img.height;
// 设置图像属性
img.set({
left: targetLeft,
top: targetTop,
scaleX: scaleX,
scaleY: scaleY,
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();
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) => {
resolve(cloned);
});
} catch (error) {
reject(error);
}
});
}
}