From c6b1bdbdf1aedff1cadd743553e1e8182a1fb5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E9=B9=8F?= <2916022834@qq.com> Date: Mon, 13 Apr 2026 11:20:26 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=A2=E7=BB=BF=E5=9B=BE=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CanvasEditor/components/ToolsSidebar.vue | 1 + src/component/Canvas/CanvasEditor/index.vue | 5 +- .../CanvasEditor/managers/ExportManager.js | 2121 +++++++++-------- .../managers/command/CommandManager.js | 14 +- 4 files changed, 1074 insertions(+), 1067 deletions(-) diff --git a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue index 69cb3214..1df8bfb3 100644 --- a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue +++ b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue @@ -55,6 +55,7 @@ commandManager.setChangeCallback((info) => { emit("undo-redo-status-changed", { canUndo: canUndo.value, canRedo: canRedo.value, + type: info.type, commandManager, }); }); diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index 1d00d0e5..b14987ef 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -907,7 +907,8 @@ } emit("changeCanvas", commandData) canvasManager.changeCanvas() - if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) { + const type = command.type + if (props.enabledRedGreenMode && (type === "undo" || type === "redo")) { setTimeout(async () => { try { const imageData = await canvasManager.exportImage({ @@ -1057,7 +1058,7 @@ } = {}) => { loading.value = true canvasManager?.canvas?.discardActiveObject() - if(isFrontBackUpdata)await canvasManager?.changeCanvas() + if (isFrontBackUpdata) await canvasManager?.changeCanvas() var base64 = await canvasManager.exportImage({ isContainBg, isContainFixed, diff --git a/src/component/Canvas/CanvasEditor/managers/ExportManager.js b/src/component/Canvas/CanvasEditor/managers/ExportManager.js index 98dc6532..eef7d462 100644 --- a/src/component/Canvas/CanvasEditor/managers/ExportManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ExportManager.js @@ -8,1077 +8,1080 @@ import { OperationType, SpecialLayerId } from "../utils/layerHelper"; * 负责处理画布的图片导出功能,支持多种导出选项和图层过滤 */ export class ExportManager { - constructor(canvasManager, layerManager) { - this.canvasManager = canvasManager; - this.layerManager = layerManager; - this.canvas = canvasManager.canvas; - } + constructor(canvasManager, layerManager) { + this.canvasManager = canvasManager; + this.layerManager = layerManager; + this.canvas = canvasManager.canvas; + } - /** - * 导出图片 - * @param {Object} options 导出选项 - * @param {Boolean} options.isContainBg 是否包含背景图层 - * @param {Boolean} options.isContainFixed 是否包含固定图层 - * @param {Boolean} options.isContainFixedOther 是否包含其他固定图层 - * @param {Boolean} options.isContainNormalLayer 是否包含普通图层 - * @param {Boolean} options.isCropByBg 是否使用背景大小裁剪 - * @param {String} options.layerId 导出具体图层ID - * @param {Array} options.layerIdArray 导出多个图层ID数组 - * @param {Array} options.layerIdArray2 导出多个图层ID数组2 - * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) - * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @param {Boolean} options.isEnhanceImg 是否是增强图片 - * @param {Array} options.excludedLayers 排除的图层ID数组 - * @returns {String} 导出的图片数据URL - */ - async exportImage(options = {}) { - const { - isContainBg = false, - isContainFixed = false, - isContainFixedOther = false, // 是否包含其他固定图层 - isContainNormalLayer = true, // 是否包含普通图层 - isCropByBg = false, // 是否使用背景大小裁剪 - layerId = "", - layerIdArray = [], - layerIdArray2 = null, - expPicType = "png", - restoreOpacityInRedGreen = true, - isEnhanceImg, // 是否是增强图片 - excludedLayers = [], // 排除的图层ID数组 - } = options; - try { + /** + * 导出图片 + * @param {Object} options 导出选项 + * @param {Boolean} options.isContainBg 是否包含背景图层 + * @param {Boolean} options.isContainFixed 是否包含固定图层 + * @param {Boolean} options.isContainFixedOther 是否包含其他固定图层 + * @param {Boolean} options.isContainNormalLayer 是否包含普通图层 + * @param {Boolean} options.isCropByBg 是否使用背景大小裁剪 + * @param {String} options.layerId 导出具体图层ID + * @param {Array} options.layerIdArray 导出多个图层ID数组 + * @param {Array} options.layerIdArray2 导出多个图层ID数组2 + * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) + * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 + * @param {Boolean} options.isEnhanceImg 是否是增强图片 + * @param {Array} options.excludedLayers 排除的图层ID数组 + * @returns {String} 导出的图片数据URL + */ + async exportImage(options = {}) { + const { + isContainBg = false, + isContainFixed = false, + isContainFixedOther = false, // 是否包含其他固定图层 + isContainNormalLayer = true, // 是否包含普通图层 + isCropByBg = false, // 是否使用背景大小裁剪 + layerId = "", + layerIdArray = [], + layerIdArray2 = null, + expPicType = "png", + restoreOpacityInRedGreen = true, + isEnhanceImg, // 是否是增强图片 + excludedLayers = [], // 排除的图层ID数组 + } = options; + try { - // 检查是否为红绿图模式 - const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false; - // 如果指定了具体图层ID,导出指定图层 - if (layerId) { - return this._exportSpecificLayer( - layerId, - expPicType, - isRedGreenMode, - restoreOpacityInRedGreen, - isCropByBg, - isEnhanceImg, // 是否是增强图片 - ); - } + // 检查是否为红绿图模式 + const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false; + // 如果指定了具体图层ID,导出指定图层 + if (layerId) { + return this._exportSpecificLayer( + layerId, + expPicType, + isRedGreenMode, + restoreOpacityInRedGreen, + isCropByBg, + isEnhanceImg, // 是否是增强图片 + ); + } - // 如果指定了多个图层ID,导出多个图层 - if (layerIdArray && layerIdArray.length > 0) { - return this._exportMultipleLayers( - layerIdArray, - expPicType, - isContainBg, - isContainFixed, - isContainFixedOther, // 是否包含其他固定图层 - isContainNormalLayer, // 是否包含普通图层 - isRedGreenMode, - restoreOpacityInRedGreen, - isCropByBg, - isEnhanceImg, // 是否是增强图片 - ); - } + // 如果指定了多个图层ID,导出多个图层 + if (layerIdArray && layerIdArray.length > 0) { + return this._exportMultipleLayers( + layerIdArray, + expPicType, + isContainBg, + isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 + isContainNormalLayer, // 是否包含普通图层 + isRedGreenMode, + restoreOpacityInRedGreen, + isCropByBg, + isEnhanceImg, // 是否是增强图片 + ); + } - // 默认导出所有可见图层 - return this._exportAllLayers( - expPicType, - isContainBg, - isContainFixed, + // 默认导出所有可见图层 + return this._exportAllLayers( + expPicType, + isContainBg, + isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 + isContainNormalLayer, // 是否包含普通图层 + isRedGreenMode, + restoreOpacityInRedGreen, + isCropByBg, + isEnhanceImg, // 是否是增强图片 + layerIdArray2, + excludedLayers, // 排除的图层ID数组 + ); + } catch (error) { + console.error("导出图片失败:", error); + throw new Error(`图片导出失败: ${error.message}`); + } + } + + /** + * 导出指定单个图层 + * @param {String} layerId 图层ID + * @param {String} expPicType 导出类型 + * @param {Boolean} isRedGreenMode 是否为红绿图模式 + * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 + * @param {Boolean} isCropByBg 是否使用背景大小裁剪 + * @param {Boolean} isEnhanceImg 是否是增强图片 + * @returns {String} 图片数据URL + * @private + */ + async _exportSpecificLayer( + layerId, + expPicType, + isRedGreenMode, + restoreOpacityInRedGreen, + isCropByBg, // 是否使用背景大小裁剪 + isEnhanceImg, // 是否是增强图片 + ) { + if (!this.layerManager) { + throw new Error("图层管理器未初始化"); + } + + const layer = this._getLayerById(layerId); + if (!layer) { + throw new Error(`未找到ID为 ${layerId} 的图层`); + } + + if (!layer.visible) { + console.warn(`图层 ${layer.name} 不可见,将导出空白图片`); + } + + // 收集所有需要导出的对象 + const objectsToExport = this._collectObjectsFromLayer(layer); + + if (objectsToExport.length === 0) { + console.warn(`图层 ${layer.name} 没有可导出的对象`); + return this._generateEmptyImage(expPicType); + } + + // 红绿图模式下使用固定尺寸和裁剪 + if (isRedGreenMode) { + return this._exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + ); + } + + // 普通模式使用画布尺寸 + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + isCropByBg, // 是否使用背景大小裁剪 + isEnhanceImg, // 是否是增强图片 + ); + } + + /** + * 导出多个指定图层 + * @param {Array} layerIdArray 图层ID数组 + * @param {String} expPicType 导出类型 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 + * @param {Boolean} isContainNormalLayer 是否包含普通图层 + * @param {Boolean} isRedGreenMode 是否为红绿图模式 + * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 + * @param {Boolean} isCropByBg 是否使用背景大小裁剪 + * @param {Boolean} isEnhanceImg 是否是增强图片 + * @returns {String} 图片数据URL + * @private + */ + async _exportMultipleLayers( + layerIdArray, + expPicType, + isContainBg, + isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 + isContainNormalLayer = true, // 是否包含普通图层 + isRedGreenMode, + restoreOpacityInRedGreen, + isCropByBg, // 是否使用背景大小裁剪 + isEnhanceImg, // 是否是增强图片 + ) { + if (!this.layerManager) { + throw new Error("图层管理器未初始化"); + } + + // 按图层顺序收集对象(从底到顶) + const objectsToExport = this._collectObjectsByLayerOrder( + layerIdArray, + isContainBg, + isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 + isContainNormalLayer, // 是否包含普通图层 + ); + + if (objectsToExport.length === 0) { + console.warn("没有可导出的对象"); + return this._generateEmptyImage(expPicType); + } + + // 红绿图模式下使用固定尺寸和裁剪 + if (isRedGreenMode) { + return this._exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); + } + + // 普通模式使用画布尺寸 + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + isCropByBg, // 是否使用背景大小裁剪 + isEnhanceImg, // 是否是增强图片 + ); + } + + /** + * 导出所有图层 + * @param {String} expPicType 导出类型 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 + * @param {Boolean} isContainNormalLayer 是否包含普通图层 + * @param {Boolean} isRedGreenMode 是否为红绿图模式 + * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 + * @param {Boolean} isCropByBg 是否使用背景大小裁剪 + * @param {Boolean} isEnhanceImg 是否是增强图片 + * @param {Array} layerIdArray 导出多个图层ID数组2 + * @returns {String} 图片数据URL + * @private + */ + async _exportAllLayers( + expPicType, + isContainBg, + isContainFixed, isContainFixedOther, // 是否包含其他固定图层 isContainNormalLayer, // 是否包含普通图层 - isRedGreenMode, - restoreOpacityInRedGreen, - isCropByBg, + isRedGreenMode, + restoreOpacityInRedGreen, + isCropByBg, // 是否使用背景大小裁剪 isEnhanceImg, // 是否是增强图片 - layerIdArray2, + layerIdArray, // 导出所有图层 excludedLayers, // 排除的图层ID数组 - ); - } catch (error) { - console.error("导出图片失败:", error); - throw new Error(`图片导出失败: ${error.message}`); - } - } + ) { + // 按图层顺序收集对象(从底到顶) + const objectsToExport = this._collectObjectsByLayerOrder( + layerIdArray, // 导出所有图层 + isContainBg, + isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 + isContainNormalLayer, // 是否包含普通图层 + excludedLayers, + ); - /** - * 导出指定单个图层 - * @param {String} layerId 图层ID - * @param {String} expPicType 导出类型 - * @param {Boolean} isRedGreenMode 是否为红绿图模式 - * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @param {Boolean} isCropByBg 是否使用背景大小裁剪 - * @param {Boolean} isEnhanceImg 是否是增强图片 - * @returns {String} 图片数据URL - * @private - */ - async _exportSpecificLayer( - layerId, - expPicType, - isRedGreenMode, - restoreOpacityInRedGreen, + if (objectsToExport.length === 0) { + console.warn("没有可导出的对象"); + return this._generateEmptyImage(expPicType); + } + + // 红绿图模式下使用固定尺寸和裁剪 + if (isRedGreenMode) { + return this._exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); + } + let canvasClipPath = this.canvas.clipPath; + if (isCropByBg) { + const cropWidth = + this.canvasManager?.canvasWidth?.value || + this.canvas?.canvasWidth || + this.canvas.width; + const cropHeight = + this.canvasManager?.canvasHeight?.value || + this.canvas?.canvasHeight || + this.canvas.height; + canvasClipPath = new fabric.Rect({ + left: this.canvas.width / 2, + top: this.canvas.height / 2, + width: cropWidth, + height: cropHeight, + originX: "center", + originY: "center", + fill: "#fff", + stroke: "transparent", + strokeWidth: 0, + }); + canvasClipPath.set({ + absolutePositioned: true, + }); + canvasClipPath.setCoords(); + } + // 普通模式使用画布尺寸 + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + canvasClipPath, + isCropByBg, // 是否使用背景大小裁剪 + isEnhanceImg, // 是否是增强图片 + ); + } + + /** + * 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象) + * @param {Object} layer 图层对象 + * @param {Boolean} isChildren 是否递归收集子图层的对象 + * @returns {Array} 画布中的真实对象数组 + * @private + */ + _collectObjectsFromLayer(layer, isChildren = true) { + if (!layer) { + return []; + } + + const realObjects = []; + + // 收集当前图层的对象 + if (layer.fabricObjects && layer.fabricObjects.length > 0) { + for (const layerObj of layer.fabricObjects) { + if (!layerObj || !layerObj.id) continue; + + // 通过ID在画布中查找真实对象 + const realObj = this._findRealObjectById(layerObj.id); + if (realObj && realObj.visible !== false) { + realObjects.push(realObj); + } + } + } + + if (layer.fabricObject) { + // 通过ID在画布中查找真实对象 + const realObj = this._findRealObjectById(layer.fabricObject.id); + if (realObj && realObj.visible !== false) { + realObjects.push(realObj); + } + } + + // 递归收集子图层的对象 + if (isChildren && layer.children && layer.children.length > 0) { + for (let i = layer.children.length - 1; i >= 0; i--) { + const childLayer = layer.children[i]; + const childObjects = this._collectObjectsFromLayer(childLayer, isChildren); + realObjects.push(...childObjects); + } + } + + return realObjects; + } + + /** + * 通过ID在画布中查找真实对象 + * @param {String} objectId 对象ID + * @returns {Object|null} 画布中的真实对象 + * @private + */ + _findRealObjectById(objectId) { + if (!objectId || !this.canvas) { + return null; + } + + try { + // 使用helper工具查找对象 + const result = findObjectById(this.canvas, objectId); + return result?.object || null; + } catch (error) { + console.warn(`查找对象 ${objectId} 失败:`, error); + return null; + } + } + + /** + * 导出对象 + * @param {Object} obj fabric对象 + * @param {String} expPicType 导出类型 + * @param {Boolean} isRedGreenMode 是否为红绿图模式 + * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 + * @returns {String} 图片数据URL + * @private + */ + async _exportObject( + obj, + expPicType, + isRedGreenMode, + restoreOpacityInRedGreen + ) { + // 红绿图模式下使用固定尺寸和裁剪 + if (isRedGreenMode) { + return this._exportWithRedGreenMode( + [obj], + expPicType, + restoreOpacityInRedGreen + ); + } + + // 普通模式使用画布尺寸 + return await this._exportWithCanvasSize( + [obj], + expPicType, + restoreOpacityInRedGreen + ); + } + + /** + * 按图层顺序收集对象(优化版本 - 从底到顶) + * @param {Array|null} layerIdArray 图层ID数组,null表示所有图层 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 + * @param {Boolean} isContainNormalLayer 是否包含普通图层 + * @param {Array} excludedLayers 排除的图层ID数组 + * @returns {Array} 按正确顺序排列的真实对象数组 + * @private + */ + _collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer, excludedLayers) { + const objectsToExport = []; + const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表 + + // 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序 + for (let i = allLayers.length - 1; i >= 0; i--) { + const layer = allLayers[i]; + + // 如果指定了图层ID数组,只处理指定的图层 + if (layerIdArray && !layerIdArray.includes(layer.id)) continue; + + // 检查图层类型过滤条件 + if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer)) + continue; + + if (layer.visible) { + const layerObjects = this._collectObjectsFromLayer(layer, false); + objectsToExport.push(...layerObjects); + } + } + + return objectsToExport; + } + + /** + * 获取扁平化的图层列表(包含子图层),排除指定的图层 + * @param {Array} excludedLayers 排除的图层ID数组 + * @returns {Array} 扁平化的图层数组 + * @private + */ + _getAllLayersFlattened(excludedLayers) { + const flattenedLayers = []; + const rootLayers = this._getAllLayers(); + + const flattenLayer = (layer) => { + // 检查是否在排除列表中 + if (excludedLayers && excludedLayers.includes(layer.id)) return; + + flattenedLayers.push(layer); + + // 递归处理子图层 + if (layer.children && layer.children.length > 0) { + for (const childLayer of layer.children) { + flattenLayer(childLayer); + } + } + }; + + // 处理所有根图层 + for (const layer of rootLayers) { + flattenLayer(layer); + } + return flattenedLayers; + } + + /** + * 计算对象组的边界 + * @param {Array} objects 对象数组 + * @returns {Object} 边界信息 {left, top, width, height} + * @private + */ + _calculateGroupBounds(objects) { + if (!objects || objects.length === 0) { + return { left: 0, top: 0, width: 1, height: 1 }; + } + + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + objects.forEach((obj) => { + if (!obj || typeof obj.getBoundingRect !== "function") { + return; + } + + const bounds = obj.getBoundingRect(); + minX = Math.min(minX, bounds.left); + minY = Math.min(minY, bounds.top); + maxX = Math.max(maxX, bounds.left + bounds.width); + maxY = Math.max(maxY, bounds.top + bounds.height); + }); + + if (minX === Infinity || minY === Infinity) { + return { left: 0, top: 0, width: 1, height: 1 }; + } + + // 添加小量边距避免边缘裁切 + const padding = 2; + return { + left: minX - padding, + top: minY - padding, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; + } + + /** + * 克隆对象并添加到临时画布,调整位置偏移 + * @param {fabric.Canvas} tempCanvas 临时画布 + * @param {Object} obj 要克隆的对象 + * @param {Object} bounds 边界信息 + * @param {Boolean} isRedGreenMode 是否为红绿图模式 + * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度 + * @returns {Promise} 克隆的对象 + * @private + */ + async _cloneAndAddObjectWithOffset( + tempCanvas, + obj, + bounds, + isRedGreenMode, + restoreOpacityInRedGreen + ) { + try { + const cloned = await this._cloneObjectForExport( + obj, + isRedGreenMode && restoreOpacityInRedGreen + ); + + if (cloned) { + // 获取对象当前边界 + const objBounds = obj.getBoundingRect(); + + // 计算相对于组边界的偏移 + const offsetX = objBounds.left - bounds.left; + const offsetY = objBounds.top - bounds.top; + + // 设置新位置(相对于临时画布的原点) + cloned.set({ + left: offsetX + objBounds.width / 2, + top: offsetY + objBounds.height / 2, + originX: "center", + originY: "center", + }); + + cloned.setCoords(); + tempCanvas.add(cloned); + return cloned; + } + } catch (error) { + console.warn(`克隆对象失败: ${obj?.id || "未知"}`, error); + } + + return null; + } + + /** + * 红绿图模式导出(使用固定图层底图作为画布尺寸和裁剪区域) + * @param {Array} objectsToExport 要导出的对象数组 + * @param {String} expPicType 导出类型 + * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1 + * @returns {String} 图片数据URL + * @private + */ + async _exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ) { + // 获取固定图层对象(衣服底图)作为参考 + const fixedLayerObject = + this._getFixedLayerObject() ?? this.canvas.clipPath; + if (!fixedLayerObject) { + console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸"); + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + ); + } + + // 使用固定图层的实际显示尺寸作为导出画布尺寸 + const canvasWidth = (fixedLayerObject.width); + const canvasHeight = (fixedLayerObject.height); + + console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`); + const tempFabricCanvas = new fabric.StaticCanvas() + tempFabricCanvas.setDimensions({ + width: canvasWidth, + height: canvasHeight, + backgroundColor: null, + // enableRetinaScaling: true, + imageSmoothingEnabled: true, + }); + // tempFabricCanvas.setZoom(1); + const ox = fixedLayerObject.left - fixedLayerObject.width * fixedLayerObject.scaleX / 2 + const oy = fixedLayerObject.top - fixedLayerObject.height * fixedLayerObject.scaleY / 2 + console.log("==========", fixedLayerObject, ox, oy) + try { + // 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层 + for (let i = 0; i < objectsToExport.length; i++) { + const obj = objectsToExport[i]; + const cloned = await this._cloneObjectForExport( + obj, + restoreOpacityInRedGreen && true + ); + if (cloned) { + let scaleX = cloned.scaleX / fixedLayerObject.scaleX + let scaleY = cloned.scaleY / fixedLayerObject.scaleY + let top = (cloned.top - oy) * scaleY + let left = (cloned.left - ox) * scaleX + cloned.set({ + left: left, + top: top, + scaleX: scaleX, + scaleY: scaleY, + }); + // 更新对象坐标 + cloned.setCoords(); + tempFabricCanvas.add(cloned); + } + } + + // 渲染画布 + tempFabricCanvas.renderAll(); + + // 生成图片 + return this._generateHighQualityDataURL(tempFabricCanvas, expPicType); + } finally { + this._cleanupTempCanvas(tempFabricCanvas); + } + } + + /** + * 普通模式导出(使用画布尺寸) + * @param {Array} objectsToExport 要导出的对象数组 + * @param {String} expPicType 导出类型 + * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1 + * @param {Object} maskObject 裁剪对象 + * @param {Boolean} isCropByBg 是否使用背景大小裁剪 + * @param {Boolean} isEnhanceImg 是否是增强图片 + * @returns {String} 图片数据URL + * @private + */ + async _exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + maskObject, // 裁剪对象 isCropByBg, // 是否使用背景大小裁剪 isEnhanceImg, // 是否是增强图片 - ) { - if (!this.layerManager) { - throw new Error("图层管理器未初始化"); - } + ) { + // 使用当前画布尺寸 + // const canvasWidth = + // this.canvasManager?.canvasWidth?.value || this.canvas.width; + // const canvasHeight = + // this.canvasManager?.canvasHeight?.value || this.canvas.height; - const layer = this._getLayerById(layerId); - if (!layer) { - throw new Error(`未找到ID为 ${layerId} 的图层`); - } - - if (!layer.visible) { - console.warn(`图层 ${layer.name} 不可见,将导出空白图片`); - } - - // 收集所有需要导出的对象 - const objectsToExport = this._collectObjectsFromLayer(layer); - - if (objectsToExport.length === 0) { - console.warn(`图层 ${layer.name} 没有可导出的对象`); - return this._generateEmptyImage(expPicType); - } - - // 红绿图模式下使用固定尺寸和裁剪 - if (isRedGreenMode) { - return this._exportWithRedGreenMode( - objectsToExport, - expPicType, - restoreOpacityInRedGreen, - ); - } - - // 普通模式使用画布尺寸 - return await this._exportWithCanvasSize( - objectsToExport, - expPicType, - restoreOpacityInRedGreen, + // console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`); + // 使用图层栅格化的方法导出图片 + const dataURL = await createRasterizedImage({ + canvas: this.canvas, + fabricObjects: objectsToExport, + format: expPicType, // 导出格式 + isReturenDataURL: true, // 返回数据URL + maskObject: maskObject ?? null, // 使用裁剪对象 + trimWhitespace: true, // 裁剪空白 + trimPadding: 0, // 裁剪边距 + restoreOpacityInRedGreen, isCropByBg, // 是否使用背景大小裁剪 isEnhanceImg, // 是否是增强图片 - ); - } - - /** - * 导出多个指定图层 - * @param {Array} layerIdArray 图层ID数组 - * @param {String} expPicType 导出类型 - * @param {Boolean} isContainBg 是否包含背景图层 - * @param {Boolean} isContainFixed 是否包含固定图层 - * @param {Boolean} isContainFixedOther 是否包含其他固定图层 - * @param {Boolean} isContainNormalLayer 是否包含普通图层 - * @param {Boolean} isRedGreenMode 是否为红绿图模式 - * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @param {Boolean} isCropByBg 是否使用背景大小裁剪 - * @param {Boolean} isEnhanceImg 是否是增强图片 - * @returns {String} 图片数据URL - * @private - */ - async _exportMultipleLayers( - layerIdArray, - expPicType, - isContainBg, - isContainFixed, - isContainFixedOther, // 是否包含其他固定图层 - isContainNormalLayer = true, // 是否包含普通图层 - isRedGreenMode, - restoreOpacityInRedGreen, - isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 - ) { - if (!this.layerManager) { - throw new Error("图层管理器未初始化"); - } - - // 按图层顺序收集对象(从底到顶) - const objectsToExport = this._collectObjectsByLayerOrder( - layerIdArray, - isContainBg, - isContainFixed, - isContainFixedOther, // 是否包含其他固定图层 - isContainNormalLayer, // 是否包含普通图层 - ); - - if (objectsToExport.length === 0) { - console.warn("没有可导出的对象"); - return this._generateEmptyImage(expPicType); - } - - // 红绿图模式下使用固定尺寸和裁剪 - if (isRedGreenMode) { - return this._exportWithRedGreenMode( - objectsToExport, - expPicType, - restoreOpacityInRedGreen - ); - } - - // 普通模式使用画布尺寸 - return await this._exportWithCanvasSize( - objectsToExport, - expPicType, - restoreOpacityInRedGreen, - isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 - ); - } - - /** - * 导出所有图层 - * @param {String} expPicType 导出类型 - * @param {Boolean} isContainBg 是否包含背景图层 - * @param {Boolean} isContainFixed 是否包含固定图层 - * @param {Boolean} isContainFixedOther 是否包含其他固定图层 - * @param {Boolean} isContainNormalLayer 是否包含普通图层 - * @param {Boolean} isRedGreenMode 是否为红绿图模式 - * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @param {Boolean} isCropByBg 是否使用背景大小裁剪 - * @param {Boolean} isEnhanceImg 是否是增强图片 - * @param {Array} layerIdArray 导出多个图层ID数组2 - * @returns {String} 图片数据URL - * @private - */ - async _exportAllLayers( - expPicType, - isContainBg, - isContainFixed, - isContainFixedOther, // 是否包含其他固定图层 - isContainNormalLayer, // 是否包含普通图层 - isRedGreenMode, - restoreOpacityInRedGreen, - isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 - layerIdArray, // 导出所有图层 - excludedLayers, // 排除的图层ID数组 - ) { - // 按图层顺序收集对象(从底到顶) - const objectsToExport = this._collectObjectsByLayerOrder( - layerIdArray, // 导出所有图层 - isContainBg, - isContainFixed, - isContainFixedOther, // 是否包含其他固定图层 - isContainNormalLayer, // 是否包含普通图层 - excludedLayers, - ); - - if (objectsToExport.length === 0) { - console.warn("没有可导出的对象"); - return this._generateEmptyImage(expPicType); - } - - // 红绿图模式下使用固定尺寸和裁剪 - if (isRedGreenMode) { - return this._exportWithRedGreenMode( - objectsToExport, - expPicType, - restoreOpacityInRedGreen - ); - } - let canvasClipPath = this.canvas.clipPath; - if (isCropByBg) { - const cropWidth = - this.canvasManager?.canvasWidth?.value || - this.canvas?.canvasWidth || - this.canvas.width; - const cropHeight = - this.canvasManager?.canvasHeight?.value || - this.canvas?.canvasHeight || - this.canvas.height; - canvasClipPath = new fabric.Rect({ - left: this.canvas.width / 2, - top: this.canvas.height / 2, - width: cropWidth, - height: cropHeight, - originX: "center", - originY: "center", - fill: "#fff", - stroke: "transparent", - strokeWidth: 0, - }); - canvasClipPath.set({ - absolutePositioned: true, - }); - canvasClipPath.setCoords(); - } - // 普通模式使用画布尺寸 - return await this._exportWithCanvasSize( - objectsToExport, - expPicType, - restoreOpacityInRedGreen, - canvasClipPath, - isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 - ); - } - - /** - * 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象) - * @param {Object} layer 图层对象 - * @param {Boolean} isChildren 是否递归收集子图层的对象 - * @returns {Array} 画布中的真实对象数组 - * @private - */ - _collectObjectsFromLayer(layer, isChildren = true) { - if (!layer) { - return []; - } - - const realObjects = []; - - // 收集当前图层的对象 - if (layer.fabricObjects && layer.fabricObjects.length > 0) { - for (const layerObj of layer.fabricObjects) { - if (!layerObj || !layerObj.id) continue; - - // 通过ID在画布中查找真实对象 - const realObj = this._findRealObjectById(layerObj.id); - if (realObj && realObj.visible !== false) { - realObjects.push(realObj); - } - } - } - - if (layer.fabricObject) { - // 通过ID在画布中查找真实对象 - const realObj = this._findRealObjectById(layer.fabricObject.id); - if (realObj && realObj.visible !== false) { - realObjects.push(realObj); - } - } - - // 递归收集子图层的对象 - if (isChildren && layer.children && layer.children.length > 0) { - for (let i = layer.children.length - 1; i >= 0; i--) { - const childLayer = layer.children[i]; - const childObjects = this._collectObjectsFromLayer(childLayer, isChildren); - realObjects.push(...childObjects); - } - } - - return realObjects; - } - - /** - * 通过ID在画布中查找真实对象 - * @param {String} objectId 对象ID - * @returns {Object|null} 画布中的真实对象 - * @private - */ - _findRealObjectById(objectId) { - if (!objectId || !this.canvas) { - return null; - } - - try { - // 使用helper工具查找对象 - const result = findObjectById(this.canvas, objectId); - return result?.object || null; - } catch (error) { - console.warn(`查找对象 ${objectId} 失败:`, error); - return null; - } - } - - /** - * 导出对象 - * @param {Object} obj fabric对象 - * @param {String} expPicType 导出类型 - * @param {Boolean} isRedGreenMode 是否为红绿图模式 - * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @returns {String} 图片数据URL - * @private - */ - async _exportObject( - obj, - expPicType, - isRedGreenMode, - restoreOpacityInRedGreen - ) { - // 红绿图模式下使用固定尺寸和裁剪 - if (isRedGreenMode) { - return this._exportWithRedGreenMode( - [obj], - expPicType, - restoreOpacityInRedGreen - ); - } - - // 普通模式使用画布尺寸 - return await this._exportWithCanvasSize( - [obj], - expPicType, - restoreOpacityInRedGreen - ); - } - - /** - * 按图层顺序收集对象(优化版本 - 从底到顶) - * @param {Array|null} layerIdArray 图层ID数组,null表示所有图层 - * @param {Boolean} isContainBg 是否包含背景图层 - * @param {Boolean} isContainFixed 是否包含固定图层 - * @param {Boolean} isContainFixedOther 是否包含其他固定图层 - * @param {Boolean} isContainNormalLayer 是否包含普通图层 - * @param {Array} excludedLayers 排除的图层ID数组 - * @returns {Array} 按正确顺序排列的真实对象数组 - * @private - */ - _collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer, excludedLayers) { - const objectsToExport = []; - const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表 - - // 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序 - for (let i = allLayers.length - 1; i >= 0; i--) { - const layer = allLayers[i]; - - // 如果指定了图层ID数组,只处理指定的图层 - if (layerIdArray && !layerIdArray.includes(layer.id)) continue; - - // 检查图层类型过滤条件 - if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer)) - continue; - - if (layer.visible) { - const layerObjects = this._collectObjectsFromLayer(layer, false); - objectsToExport.push(...layerObjects); - } - } - - return objectsToExport; - } - - /** - * 获取扁平化的图层列表(包含子图层),排除指定的图层 - * @param {Array} excludedLayers 排除的图层ID数组 - * @returns {Array} 扁平化的图层数组 - * @private - */ - _getAllLayersFlattened(excludedLayers) { - const flattenedLayers = []; - const rootLayers = this._getAllLayers(); - - const flattenLayer = (layer) => { - // 检查是否在排除列表中 - if (excludedLayers && excludedLayers.includes(layer.id)) return; - - flattenedLayers.push(layer); - - // 递归处理子图层 - if (layer.children && layer.children.length > 0) { - for (const childLayer of layer.children) { - flattenLayer(childLayer); - } - } - }; - - // 处理所有根图层 - for (const layer of rootLayers) { - flattenLayer(layer); - } - return flattenedLayers; - } - - /** - * 计算对象组的边界 - * @param {Array} objects 对象数组 - * @returns {Object} 边界信息 {left, top, width, height} - * @private - */ - _calculateGroupBounds(objects) { - if (!objects || objects.length === 0) { - return { left: 0, top: 0, width: 1, height: 1 }; - } - - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - - objects.forEach((obj) => { - if (!obj || typeof obj.getBoundingRect !== "function") { - return; - } - - const bounds = obj.getBoundingRect(); - minX = Math.min(minX, bounds.left); - minY = Math.min(minY, bounds.top); - maxX = Math.max(maxX, bounds.left + bounds.width); - maxY = Math.max(maxY, bounds.top + bounds.height); - }); - - if (minX === Infinity || minY === Infinity) { - return { left: 0, top: 0, width: 1, height: 1 }; - } - - // 添加小量边距避免边缘裁切 - const padding = 2; - return { - left: minX - padding, - top: minY - padding, - width: maxX - minX + padding * 2, - height: maxY - minY + padding * 2, - }; - } - - /** - * 克隆对象并添加到临时画布,调整位置偏移 - * @param {fabric.Canvas} tempCanvas 临时画布 - * @param {Object} obj 要克隆的对象 - * @param {Object} bounds 边界信息 - * @param {Boolean} isRedGreenMode 是否为红绿图模式 - * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度 - * @returns {Promise} 克隆的对象 - * @private - */ - async _cloneAndAddObjectWithOffset( - tempCanvas, - obj, - bounds, - isRedGreenMode, - restoreOpacityInRedGreen - ) { - try { - const cloned = await this._cloneObjectForExport( - obj, - isRedGreenMode && restoreOpacityInRedGreen - ); - - if (cloned) { - // 获取对象当前边界 - const objBounds = obj.getBoundingRect(); - - // 计算相对于组边界的偏移 - const offsetX = objBounds.left - bounds.left; - const offsetY = objBounds.top - bounds.top; - - // 设置新位置(相对于临时画布的原点) - cloned.set({ - left: offsetX + objBounds.width / 2, - top: offsetY + objBounds.height / 2, - originX: "center", - originY: "center", - }); - - cloned.setCoords(); - tempCanvas.add(cloned); - return cloned; - } - } catch (error) { - console.warn(`克隆对象失败: ${obj?.id || "未知"}`, error); - } - - return null; - } - - /** - * 红绿图模式导出(使用固定图层底图作为画布尺寸和裁剪区域) - * @param {Array} objectsToExport 要导出的对象数组 - * @param {String} expPicType 导出类型 - * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1 - * @returns {String} 图片数据URL - * @private - */ - async _exportWithRedGreenMode( - objectsToExport, - expPicType, - restoreOpacityInRedGreen - ) { - // 获取固定图层对象(衣服底图)作为参考 - const fixedLayerObject = - this._getFixedLayerObject() ?? this.canvas.clipPath; - if (!fixedLayerObject) { - console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸"); - return await this._exportWithCanvasSize( - objectsToExport, - expPicType, - restoreOpacityInRedGreen, - ); - } - - // 使用固定图层的实际显示尺寸作为导出画布尺寸 - const canvasWidth = (fixedLayerObject.width); - const canvasHeight = (fixedLayerObject.height); - - console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`); - const tempFabricCanvas = new fabric.StaticCanvas() - tempFabricCanvas.setDimensions({ - width: canvasWidth, - height: canvasHeight, - backgroundColor: null, - // enableRetinaScaling: true, - imageSmoothingEnabled: true, - }); - // tempFabricCanvas.setZoom(1); - console.log("==========", fixedLayerObject) - try { - // 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层 - for (let i = 0; i < objectsToExport.length; i++) { - const obj = objectsToExport[i]; - const cloned = await this._cloneObjectForExport( - obj, - restoreOpacityInRedGreen && true - ); - if (cloned) { - cloned.set({ - left: canvasWidth / 2, - top: canvasHeight / 2, - scaleX: cloned.scaleX / fixedLayerObject.scaleX, - scaleY: cloned.scaleY / fixedLayerObject.scaleY, - originX: "center", - originY: "center", - }); - console.log("==========", {...cloned}) - // 更新对象坐标 - cloned.setCoords(); - tempFabricCanvas.add(cloned); - } - } - - // 渲染画布 - tempFabricCanvas.renderAll(); - - // 生成图片 - return this._generateHighQualityDataURL(tempFabricCanvas, expPicType); - } finally { - this._cleanupTempCanvas(tempFabricCanvas); - } - } - - /** - * 普通模式导出(使用画布尺寸) - * @param {Array} objectsToExport 要导出的对象数组 - * @param {String} expPicType 导出类型 - * @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1 - * @param {Object} maskObject 裁剪对象 - * @param {Boolean} isCropByBg 是否使用背景大小裁剪 - * @param {Boolean} isEnhanceImg 是否是增强图片 - * @returns {String} 图片数据URL - * @private - */ - async _exportWithCanvasSize( - objectsToExport, - expPicType, - restoreOpacityInRedGreen, - maskObject, // 裁剪对象 - isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 - ) { - // 使用当前画布尺寸 - // const canvasWidth = - // this.canvasManager?.canvasWidth?.value || this.canvas.width; - // const canvasHeight = - // this.canvasManager?.canvasHeight?.value || this.canvas.height; - - // console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`); - // 使用图层栅格化的方法导出图片 - const dataURL = await createRasterizedImage({ - canvas: this.canvas, - fabricObjects: objectsToExport, - format: expPicType, // 导出格式 - isReturenDataURL: true, // 返回数据URL - maskObject: maskObject ?? null, // 使用裁剪对象 - trimWhitespace: true, // 裁剪空白 - trimPadding: 0, // 裁剪边距 - restoreOpacityInRedGreen, - isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 - }); - - // console.log("导出图片数据URL:", dataURL); - return dataURL; - - // // 创建与画布相同尺寸的临时画布 - // const scaleFactor = 2; // 高清导出 - // const tempCanvas = document.createElement("canvas"); - // tempCanvas.width = canvasWidth * scaleFactor; - // tempCanvas.height = canvasHeight * scaleFactor; - // tempCanvas.style.width = canvasWidth + "px"; - // tempCanvas.style.height = canvasHeight + "px"; - - // const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, { - // width: canvasWidth, - // height: canvasHeight, - // backgroundColor: null, - // }); - - // tempFabricCanvas.enableRetinaScaling = true; - // tempFabricCanvas.imageSmoothingEnabled = true; - // tempFabricCanvas.setZoom(1); - - // try { - // // 克隆并添加所有对象到临时画布 - // for (const obj of objectsToExport) { - // const cloned = await this._cloneObjectForExport( - // obj, - // restoreOpacityInRedGreen && false, // 普通模式不强制恢复透明度 - // ); - // if (cloned) { - // tempFabricCanvas.add(cloned); - // } - // } - - // // 渲染画布 - // tempFabricCanvas.renderAll(); - - // // 生成图片 - // return this._generateHighQualityDataURL(tempCanvas, expPicType); - // } finally { - // this._cleanupTempCanvas(tempFabricCanvas); - // } - } - - /** - * 获取固定图层对象 - * @returns {Object|null} 固定图层对象 - * @private - */ - _getFixedLayerObject() { - const allLayers = this._getAllLayers(); - const fixedLayer = allLayers.find((layer) => layer.isFixed); - - if (!fixedLayer || !fixedLayer.fabricObject) { - return null; - } - - // 如果有ID,通过ID查找画布中的实际对象 - if (fixedLayer.fabricObject.id) { - const result = findObjectById(this.canvas, fixedLayer.fabricObject.id); - return result.object || fixedLayer.fabricObject; - } - - return fixedLayer.fabricObject; - } - - /** - * 异步克隆fabric对象(参照createRasterizedImage的方法) - * @param {fabric.Object} obj 要克隆的对象 - * @param {Array} propertiesToInclude 要包含的属性 - * @returns {Promise} 克隆的对象 - * @private - */ - _cloneObjectAsync( - obj, - propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"] - ) { - return new Promise((resolve, reject) => { - if (!obj) { - resolve(null); - return; - } - - try { - obj.clone((cloned) => { - if (cloned) { - resolve(cloned); - } else { - reject(new Error("对象克隆失败")); - } - }, propertiesToInclude); - } catch (error) { - console.warn("克隆对象失败:", error); - resolve(null); - } - }); - } - - /** - * 克隆对象用于导出(优化版本) - * @param {Object} obj fabric对象 - * @param {Boolean} forceRestoreOpacity 是否强制恢复透明度为1 - * @param {Boolean} removeClipPath 是否移除裁剪路径 - * @returns {Promise} 克隆的对象 - * @private - */ - async _cloneObjectForExport( - obj, - forceRestoreOpacity = false, - removeClipPath = true - ) { - if (!obj) return null; - - try { - // 使用异步克隆方法 - const cloned = await this._cloneObjectAsync(obj); - - if (cloned) { - // 保持原始位置和属性 - cloned.set({ - selectable: false, - evented: false, - visible: true, - }); - - // 如果需要恢复透明度 - if (forceRestoreOpacity) { - cloned.set({ opacity: 1 }); - } - - // 移除裁剪路径以避免绝对路径问题 - if (removeClipPath && cloned.clipPath) { - console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`); - cloned.clipPath = null; - } - - return cloned; - } - } catch (error) { - console.warn("克隆对象失败:", error); - } - - return null; - } - - /** - * 导出对象组 - * @param {Array} objectsToExport 要导出的对象数组 - * @param {String} expPicType 导出类型 - * @param {Boolean} isRedGreenMode 是否为红绿图模式 - * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @returns {Promise} 图片数据URL - * @private - */ - async _exportObjectsAsGroup( - objectsToExport, - expPicType, - isRedGreenMode = false, - restoreOpacityInRedGreen = true - ) { - if (!objectsToExport || objectsToExport.length === 0) { - throw new Error("没有可导出的对象"); - } - - // 计算所有对象的边界 - const bounds = this._calculateGroupBounds(objectsToExport); - console.log("导出边界:", bounds); - - // 创建高质量临时画布 - const scaleFactor = 2; // 高清导出 - const tempCanvas = document.createElement("canvas"); - tempCanvas.width = bounds.width * scaleFactor; - tempCanvas.height = bounds.height * scaleFactor; - tempCanvas.style.width = bounds.width + "px"; - tempCanvas.style.height = bounds.height + "px"; - - const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, { - width: bounds.width, - height: bounds.height, - backgroundColor: null, // 透明背景 - }); - - // 启用高清缩放和图像平滑 - tempFabricCanvas.enableRetinaScaling = true; - tempFabricCanvas.imageSmoothingEnabled = true; - tempFabricCanvas.setZoom(scaleFactor); - - try { - // 克隆所有对象并添加到临时画布 - const clonedObjects = []; - for (const obj of objectsToExport) { - const cloned = await this._cloneAndAddObjectWithOffset( - tempFabricCanvas, - obj, - bounds, - isRedGreenMode, - restoreOpacityInRedGreen - ); - if (cloned) { - clonedObjects.push(cloned); - } - } - - console.log(`成功克隆 ${clonedObjects.length} 个对象进行导出`); - - // 渲染画布 - tempFabricCanvas.renderAll(); - - // 生成高质量数据URL - return this._generateHighQualityDataURL(tempCanvas, expPicType); - } finally { - this._cleanupTempCanvas(tempFabricCanvas); - } - } - - /** - * 获取裁剪路径对象(优化版本) - * @param {Object} fixedBounds 固定图层边界 - * @returns {Promise} 裁剪路径对象 - * @private - */ - async _getClipPathObject(fixedBounds) { - try { - // const allLayers = this._getAllLayers(); - - // // 查找第一个有裁剪遮罩的图层 - // let clipObject = null; - - // for (const layer of allLayers) { - // if (layer.clippingMask?.id) { - // const result = findObjectById(this.canvas, layer.clippingMask.id); - // if (result?.object) { - // clipObject = result.object; - // break; - // } - // } - // } - const clipObject = this.canvas?.clipPath; - if (!clipObject) { - console.warn("未找到可用的裁剪对象"); - return null; - } - - // 克隆对象作为裁剪路径 - const clonedClipPath = await this._cloneObjectForExport( - clipObject, - false, - false - ); - - if (!clonedClipPath) { - console.warn("无法克隆裁剪对象"); - return null; - } - - // 调整裁剪路径的位置相对于固定图层 - clonedClipPath.set({ - left: clonedClipPath.left - fixedBounds.left, - top: clonedClipPath.top - fixedBounds.top, - absolutePositioned: true, // 使用绝对定位 - }); - - // 更新坐标 - clonedClipPath.setCoords(); - - console.log("成功创建裁剪路径:", { - objectType: clonedClipPath.type, - position: { left: clonedClipPath.left, top: clonedClipPath.top }, - size: { width: clonedClipPath.width, height: clonedClipPath.height }, - }); - - return clonedClipPath; - } catch (error) { - console.error("获取裁剪路径失败:", error); - return null; - } - } - - /** - * 生成高质量数据URL - * @param {HTMLCanvasElement} canvas 画布元素 - * @param {String} expPicType 导出类型 - * @returns {String} 数据URL - * @private - */ - _generateHighQualityDataURL(canvas, expPicType) { - const format = expPicType.toLowerCase(); - - switch (format) { - case "jpg": - case "jpeg": - // 对于JPEG,使用较高质量,但JPEG不支持透明背景 - return canvas.toDataURL("image/jpeg", 0.95); - case "svg": - // SVG导出需要特殊处理,这里先返回高质量PNG - console.warn("SVG导出暂未实现,返回高质量PNG格式"); - return canvas.toDataURL("image/png", 1.0); - case "png": - default: - // PNG使用最高质量,支持透明背景 - return canvas.toDataURL("image/png", 1.0); - } - } - - /** - * 生成空白图片 - * @param {String} expPicType 导出类型 - * @returns {String} 空白图片数据URL - * @private - */ - _generateEmptyImage(expPicType) { - const emptyCanvas = document.createElement("canvas"); - emptyCanvas.width = 1; - emptyCanvas.height = 1; - - // 确保透明背景 - const ctx = emptyCanvas.getContext("2d"); - ctx.clearRect(0, 0, 1, 1); - - return this._generateHighQualityDataURL(emptyCanvas, expPicType); - } - - /** - * 清理临时画布资源 - * @param {fabric.StaticCanvas} tempFabricCanvas 临时Fabric画布 - * @private - */ - _cleanupTempCanvas(tempFabricCanvas) { - if (tempFabricCanvas) { - try { - tempFabricCanvas.dispose(); - } catch (error) { - console.warn("清理临时画布失败:", error); - } - } - } - - /** - * 获取所有图层 - * @returns {Array} 图层数组 - * @private - */ - _getAllLayers() { - if (this.layerManager && this.layerManager.layers) { - return this.layerManager.layers.value || []; - } - return []; - } - - /** - * 根据ID获取图层 - * @param {String} layerId 图层ID - * @returns {Object|null} 图层对象 - * @private - */ - _getLayerById(layerId) { - if (this.layerManager && this.layerManager.getLayerById) { - return this.layerManager.getLayerById(layerId); - } - - // 备用方法:直接从图层数组中查找 - const allLayers = this._getAllLayers(); - return allLayers.find((layer) => layer.id === layerId) || null; - } - - /** - * 检查图层是否应该包含在导出中 - * @param {Object} layer 图层对象 - * @param {Boolean} isContainBg 是否包含背景图层 - * @param {Boolean} isContainFixed 是否包含固定图层 - * @param {Boolean} isContainFixedOther 是否包含其他固定图层 - * @param {Boolean} isContainNormalLayer 是否包含普通图层 - * @returns {Boolean} 是否应该包含 - * @private - */ - _shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer) { - if (!layer) return false; - - // 检查背景图层 - if (layer.isBackground) { - return isContainBg; - } - - // 检查固定图层 - if (layer.isFixed) { - return isContainFixed; - } - - // 检查其他固定图层 - if (layer.isFixedOther) { - return isContainFixedOther; - } - - // 印花图层始终导出 - if (layer.isPrintTrims || layer.isPrintTrimsGroup) { - return true; - } - - // 普通图层 - return isContainNormalLayer; - } + }); + + // console.log("导出图片数据URL:", dataURL); + return dataURL; + + // // 创建与画布相同尺寸的临时画布 + // const scaleFactor = 2; // 高清导出 + // const tempCanvas = document.createElement("canvas"); + // tempCanvas.width = canvasWidth * scaleFactor; + // tempCanvas.height = canvasHeight * scaleFactor; + // tempCanvas.style.width = canvasWidth + "px"; + // tempCanvas.style.height = canvasHeight + "px"; + + // const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, { + // width: canvasWidth, + // height: canvasHeight, + // backgroundColor: null, + // }); + + // tempFabricCanvas.enableRetinaScaling = true; + // tempFabricCanvas.imageSmoothingEnabled = true; + // tempFabricCanvas.setZoom(1); + + // try { + // // 克隆并添加所有对象到临时画布 + // for (const obj of objectsToExport) { + // const cloned = await this._cloneObjectForExport( + // obj, + // restoreOpacityInRedGreen && false, // 普通模式不强制恢复透明度 + // ); + // if (cloned) { + // tempFabricCanvas.add(cloned); + // } + // } + + // // 渲染画布 + // tempFabricCanvas.renderAll(); + + // // 生成图片 + // return this._generateHighQualityDataURL(tempCanvas, expPicType); + // } finally { + // this._cleanupTempCanvas(tempFabricCanvas); + // } + } + + /** + * 获取固定图层对象 + * @returns {Object|null} 固定图层对象 + * @private + */ + _getFixedLayerObject() { + const allLayers = this._getAllLayers(); + const fixedLayer = allLayers.find((layer) => layer.isFixed); + + if (!fixedLayer || !fixedLayer.fabricObject) { + return null; + } + + // 如果有ID,通过ID查找画布中的实际对象 + if (fixedLayer.fabricObject.id) { + const result = findObjectById(this.canvas, fixedLayer.fabricObject.id); + return result.object || fixedLayer.fabricObject; + } + + return fixedLayer.fabricObject; + } + + /** + * 异步克隆fabric对象(参照createRasterizedImage的方法) + * @param {fabric.Object} obj 要克隆的对象 + * @param {Array} propertiesToInclude 要包含的属性 + * @returns {Promise} 克隆的对象 + * @private + */ + _cloneObjectAsync( + obj, + propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"] + ) { + return new Promise((resolve, reject) => { + if (!obj) { + resolve(null); + return; + } + + try { + obj.clone((cloned) => { + if (cloned) { + resolve(cloned); + } else { + reject(new Error("对象克隆失败")); + } + }, propertiesToInclude); + } catch (error) { + console.warn("克隆对象失败:", error); + resolve(null); + } + }); + } + + /** + * 克隆对象用于导出(优化版本) + * @param {Object} obj fabric对象 + * @param {Boolean} forceRestoreOpacity 是否强制恢复透明度为1 + * @param {Boolean} removeClipPath 是否移除裁剪路径 + * @returns {Promise} 克隆的对象 + * @private + */ + async _cloneObjectForExport( + obj, + forceRestoreOpacity = false, + removeClipPath = true + ) { + if (!obj) return null; + + try { + // 使用异步克隆方法 + const cloned = await this._cloneObjectAsync(obj); + + if (cloned) { + // 保持原始位置和属性 + cloned.set({ + selectable: false, + evented: false, + visible: true, + }); + + // 如果需要恢复透明度 + if (forceRestoreOpacity) { + cloned.set({ opacity: 1 }); + } + + // 移除裁剪路径以避免绝对路径问题 + if (removeClipPath && cloned.clipPath) { + console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`); + cloned.clipPath = null; + } + + return cloned; + } + } catch (error) { + console.warn("克隆对象失败:", error); + } + + return null; + } + + /** + * 导出对象组 + * @param {Array} objectsToExport 要导出的对象数组 + * @param {String} expPicType 导出类型 + * @param {Boolean} isRedGreenMode 是否为红绿图模式 + * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 + * @returns {Promise} 图片数据URL + * @private + */ + async _exportObjectsAsGroup( + objectsToExport, + expPicType, + isRedGreenMode = false, + restoreOpacityInRedGreen = true + ) { + if (!objectsToExport || objectsToExport.length === 0) { + throw new Error("没有可导出的对象"); + } + + // 计算所有对象的边界 + const bounds = this._calculateGroupBounds(objectsToExport); + console.log("导出边界:", bounds); + + // 创建高质量临时画布 + const scaleFactor = 2; // 高清导出 + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = bounds.width * scaleFactor; + tempCanvas.height = bounds.height * scaleFactor; + tempCanvas.style.width = bounds.width + "px"; + tempCanvas.style.height = bounds.height + "px"; + + const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, { + width: bounds.width, + height: bounds.height, + backgroundColor: null, // 透明背景 + }); + + // 启用高清缩放和图像平滑 + tempFabricCanvas.enableRetinaScaling = true; + tempFabricCanvas.imageSmoothingEnabled = true; + tempFabricCanvas.setZoom(scaleFactor); + + try { + // 克隆所有对象并添加到临时画布 + const clonedObjects = []; + for (const obj of objectsToExport) { + const cloned = await this._cloneAndAddObjectWithOffset( + tempFabricCanvas, + obj, + bounds, + isRedGreenMode, + restoreOpacityInRedGreen + ); + if (cloned) { + clonedObjects.push(cloned); + } + } + + console.log(`成功克隆 ${clonedObjects.length} 个对象进行导出`); + + // 渲染画布 + tempFabricCanvas.renderAll(); + + // 生成高质量数据URL + return this._generateHighQualityDataURL(tempCanvas, expPicType); + } finally { + this._cleanupTempCanvas(tempFabricCanvas); + } + } + + /** + * 获取裁剪路径对象(优化版本) + * @param {Object} fixedBounds 固定图层边界 + * @returns {Promise} 裁剪路径对象 + * @private + */ + async _getClipPathObject(fixedBounds) { + try { + // const allLayers = this._getAllLayers(); + + // // 查找第一个有裁剪遮罩的图层 + // let clipObject = null; + + // for (const layer of allLayers) { + // if (layer.clippingMask?.id) { + // const result = findObjectById(this.canvas, layer.clippingMask.id); + // if (result?.object) { + // clipObject = result.object; + // break; + // } + // } + // } + const clipObject = this.canvas?.clipPath; + if (!clipObject) { + console.warn("未找到可用的裁剪对象"); + return null; + } + + // 克隆对象作为裁剪路径 + const clonedClipPath = await this._cloneObjectForExport( + clipObject, + false, + false + ); + + if (!clonedClipPath) { + console.warn("无法克隆裁剪对象"); + return null; + } + + // 调整裁剪路径的位置相对于固定图层 + clonedClipPath.set({ + left: clonedClipPath.left - fixedBounds.left, + top: clonedClipPath.top - fixedBounds.top, + absolutePositioned: true, // 使用绝对定位 + }); + + // 更新坐标 + clonedClipPath.setCoords(); + + console.log("成功创建裁剪路径:", { + objectType: clonedClipPath.type, + position: { left: clonedClipPath.left, top: clonedClipPath.top }, + size: { width: clonedClipPath.width, height: clonedClipPath.height }, + }); + + return clonedClipPath; + } catch (error) { + console.error("获取裁剪路径失败:", error); + return null; + } + } + + /** + * 生成高质量数据URL + * @param {HTMLCanvasElement} canvas 画布元素 + * @param {String} expPicType 导出类型 + * @returns {String} 数据URL + * @private + */ + _generateHighQualityDataURL(canvas, expPicType) { + const format = expPicType.toLowerCase(); + + switch (format) { + case "jpg": + case "jpeg": + // 对于JPEG,使用较高质量,但JPEG不支持透明背景 + return canvas.toDataURL("image/jpeg", 0.95); + case "svg": + // SVG导出需要特殊处理,这里先返回高质量PNG + console.warn("SVG导出暂未实现,返回高质量PNG格式"); + return canvas.toDataURL("image/png", 1.0); + case "png": + default: + // PNG使用最高质量,支持透明背景 + return canvas.toDataURL("image/png", 1.0); + } + } + + /** + * 生成空白图片 + * @param {String} expPicType 导出类型 + * @returns {String} 空白图片数据URL + * @private + */ + _generateEmptyImage(expPicType) { + const emptyCanvas = document.createElement("canvas"); + emptyCanvas.width = 1; + emptyCanvas.height = 1; + + // 确保透明背景 + const ctx = emptyCanvas.getContext("2d"); + ctx.clearRect(0, 0, 1, 1); + + return this._generateHighQualityDataURL(emptyCanvas, expPicType); + } + + /** + * 清理临时画布资源 + * @param {fabric.StaticCanvas} tempFabricCanvas 临时Fabric画布 + * @private + */ + _cleanupTempCanvas(tempFabricCanvas) { + if (tempFabricCanvas) { + try { + tempFabricCanvas.dispose(); + } catch (error) { + console.warn("清理临时画布失败:", error); + } + } + } + + /** + * 获取所有图层 + * @returns {Array} 图层数组 + * @private + */ + _getAllLayers() { + if (this.layerManager && this.layerManager.layers) { + return this.layerManager.layers.value || []; + } + return []; + } + + /** + * 根据ID获取图层 + * @param {String} layerId 图层ID + * @returns {Object|null} 图层对象 + * @private + */ + _getLayerById(layerId) { + if (this.layerManager && this.layerManager.getLayerById) { + return this.layerManager.getLayerById(layerId); + } + + // 备用方法:直接从图层数组中查找 + const allLayers = this._getAllLayers(); + return allLayers.find((layer) => layer.id === layerId) || null; + } + + /** + * 检查图层是否应该包含在导出中 + * @param {Object} layer 图层对象 + * @param {Boolean} isContainBg 是否包含背景图层 + * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 + * @param {Boolean} isContainNormalLayer 是否包含普通图层 + * @returns {Boolean} 是否应该包含 + * @private + */ + _shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer) { + if (!layer) return false; + + // 检查背景图层 + if (layer.isBackground) { + return isContainBg; + } + + // 检查固定图层 + if (layer.isFixed) { + return isContainFixed; + } + + // 检查其他固定图层 + if (layer.isFixedOther) { + return isContainFixedOther; + } + + // 印花图层始终导出 + if (layer.isPrintTrims || layer.isPrintTrimsGroup) { + return true; + } + + // 普通图层 + return isContainNormalLayer; + } } diff --git a/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js b/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js index 65fd54b4..f88e3d36 100644 --- a/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js +++ b/src/component/Canvas/CanvasEditor/managers/command/CommandManager.js @@ -180,7 +180,7 @@ export class CommandManager { this._recordPerformance("execute", command.constructor.name, duration); // 通知状态变化 - this._notifyStateChange(); + this._notifyStateChange("execute"); console.log(`✅ 命令执行成功: ${command.constructor.name}`); return result; @@ -219,7 +219,7 @@ export class CommandManager { this._recordPerformance("undo", command.constructor.name, duration); // 通知状态变化 - this._notifyStateChange(); + this._notifyStateChange("undo"); console.log(`✅ 命令撤销成功: ${command.constructor.name}`); return result; @@ -258,7 +258,7 @@ export class CommandManager { this._recordPerformance("redo", command.constructor.name, duration); // 通知状态变化 - this._notifyStateChange(); + this._notifyStateChange("redo"); console.log(`✅ 命令重做成功: ${command.constructor.name}`); return result; @@ -298,7 +298,7 @@ export class CommandManager { this.undoStack = []; this.redoStack = []; - this._notifyStateChange(); + this._notifyStateChange("clear"); // console.log("📝 命令历史已清空"); } @@ -417,10 +417,12 @@ export class CommandManager { * 通知状态变化 * @private */ - _notifyStateChange() { + _notifyStateChange(type) { if (this.onStateChange) { try { - this.onStateChange(this.getState()); + const obj = this.getState(); + obj.type = type; + this.onStateChange(obj); } catch (error) { console.error("状态变化回调执行失败:", error); }