504 lines
15 KiB
JavaScript
504 lines
15 KiB
JavaScript
import { createRasterizedImage } from "../utils/selectionToImage.js";
|
||
import { CompositeCommand } from "./Command.js";
|
||
import { CreateImageLayerCommand } from "./LayerCommands.js";
|
||
import { ClearSelectionCommand } from "./SelectionCommands.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 || 2; // 基础分辨率倍数
|
||
}
|
||
|
||
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) {
|
||
console.error("无法执行套索抠图:源图层无效");
|
||
return false;
|
||
}
|
||
|
||
// 获取源图层的所有对象(包括子图层)
|
||
const sourceObjects = this._getLayerObjects(sourceLayer);
|
||
if (sourceObjects.length === 0) {
|
||
console.error("无法执行套索抠图:源图层没有可见对象");
|
||
return false;
|
||
}
|
||
|
||
// 获取选区边界信息用于后续定位
|
||
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);
|
||
|
||
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} 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, // 启用高质量模式
|
||
});
|
||
|
||
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) => {
|
||
resolve(cloned);
|
||
});
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
}
|