492 lines
15 KiB
JavaScript
492 lines
15 KiB
JavaScript
|
|
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);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|