2025-06-18 11:05:23 +08:00
|
|
|
|
import { fabric } from "fabric-with-all";
|
|
|
|
|
|
import { findObjectById } from "../utils/helper";
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 图片导出管理器
|
|
|
|
|
|
* 负责处理画布的图片导出功能,支持多种导出选项和图层过滤
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class ExportManager {
|
|
|
|
|
|
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 {String} options.layerId 导出具体图层ID
|
|
|
|
|
|
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
|
|
|
|
|
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @returns {String} 导出的图片数据URL
|
|
|
|
|
|
*/
|
|
|
|
|
|
exportImage(options = {}) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
isContainBg = false,
|
|
|
|
|
|
isContainFixed = false,
|
|
|
|
|
|
layerId = "",
|
|
|
|
|
|
layerIdArray = [],
|
2025-06-18 11:05:23 +08:00
|
|
|
|
expPicType = "png",
|
|
|
|
|
|
restoreOpacityInRedGreen = true,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 检查是否为红绿图模式
|
|
|
|
|
|
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 如果指定了具体图层ID,导出指定图层
|
|
|
|
|
|
if (layerId) {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return this._exportSpecificLayer(
|
|
|
|
|
|
layerId,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果指定了多个图层ID,导出多个图层
|
|
|
|
|
|
if (layerIdArray && layerIdArray.length > 0) {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return this._exportMultipleLayers(
|
|
|
|
|
|
layerIdArray,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
isContainBg,
|
|
|
|
|
|
isContainFixed,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 默认导出所有可见图层
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return this._exportAllLayers(
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
isContainBg,
|
|
|
|
|
|
isContainFixed,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("导出图片失败:", error);
|
|
|
|
|
|
throw new Error(`图片导出失败: ${error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导出指定单个图层
|
|
|
|
|
|
* @param {String} layerId 图层ID
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @returns {String} 图片数据URL
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
_exportSpecificLayer(
|
|
|
|
|
|
layerId,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
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} 不可见,将导出空白图片`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 收集所有需要导出的对象
|
|
|
|
|
|
const objectsToExport = this._collectObjectsFromLayer(layer);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
if (objectsToExport.length === 0) {
|
|
|
|
|
|
console.warn(`图层 ${layer.name} 没有可导出的对象`);
|
|
|
|
|
|
return this._generateEmptyImage(expPicType);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 红绿图模式下使用固定尺寸和裁剪
|
|
|
|
|
|
if (isRedGreenMode) {
|
|
|
|
|
|
return this._exportWithRedGreenMode(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 普通模式使用画布尺寸
|
|
|
|
|
|
return this._exportWithCanvasSize(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导出多个指定图层
|
|
|
|
|
|
* @param {Array} layerIdArray 图层ID数组
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
|
|
|
|
|
* @param {Boolean} isContainBg 是否包含背景图层
|
|
|
|
|
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @returns {String} 图片数据URL
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
_exportMultipleLayers(
|
|
|
|
|
|
layerIdArray,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
isContainBg,
|
|
|
|
|
|
isContainFixed,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
if (!this.layerManager) {
|
|
|
|
|
|
throw new Error("图层管理器未初始化");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 按图层顺序收集对象(从底到顶)
|
|
|
|
|
|
const objectsToExport = this._collectObjectsByLayerOrder(
|
|
|
|
|
|
layerIdArray,
|
|
|
|
|
|
isContainBg,
|
|
|
|
|
|
isContainFixed
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
if (objectsToExport.length === 0) {
|
|
|
|
|
|
console.warn("没有可导出的对象");
|
|
|
|
|
|
return this._generateEmptyImage(expPicType);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 红绿图模式下使用固定尺寸和裁剪
|
|
|
|
|
|
if (isRedGreenMode) {
|
|
|
|
|
|
return this._exportWithRedGreenMode(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 普通模式使用画布尺寸
|
|
|
|
|
|
return this._exportWithCanvasSize(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导出所有图层
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
|
|
|
|
|
* @param {Boolean} isContainBg 是否包含背景图层
|
|
|
|
|
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @returns {String} 图片数据URL
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
_exportAllLayers(
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
isContainBg,
|
|
|
|
|
|
isContainFixed,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 按图层顺序收集对象(从底到顶)
|
|
|
|
|
|
const objectsToExport = this._collectObjectsByLayerOrder(
|
|
|
|
|
|
null, // 导出所有图层
|
|
|
|
|
|
isContainBg,
|
|
|
|
|
|
isContainFixed
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (objectsToExport.length === 0) {
|
|
|
|
|
|
console.warn("没有可导出的对象");
|
|
|
|
|
|
return this._generateEmptyImage(expPicType);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 红绿图模式下使用固定尺寸和裁剪
|
|
|
|
|
|
if (isRedGreenMode) {
|
|
|
|
|
|
return this._exportWithRedGreenMode(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 普通模式使用画布尺寸
|
|
|
|
|
|
return this._exportWithCanvasSize(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 按图层顺序收集对象(从底到顶)
|
|
|
|
|
|
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @param {Boolean} isContainBg 是否包含背景图层
|
|
|
|
|
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* @returns {Array} 按正确顺序排列的对象数组
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
|
|
|
|
|
|
const objectsToExport = [];
|
|
|
|
|
|
const allLayers = this._getAllLayers();
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
|
|
|
|
|
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))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (layer.visible) {
|
|
|
|
|
|
const layerObjects = this._collectObjectsFromLayer(layer);
|
|
|
|
|
|
objectsToExport.push(...layerObjects);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return objectsToExport;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 红绿图模式导出(使用固定图层底图作为画布尺寸和裁剪区域)
|
|
|
|
|
|
* @param {Array} objectsToExport 要导出的对象数组
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
|
|
|
|
|
|
* @returns {String} 图片数据URL
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
async _exportWithRedGreenMode(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 获取固定图层对象(衣服底图)作为参考
|
|
|
|
|
|
const fixedLayerObject = this._getFixedLayerObject();
|
|
|
|
|
|
if (!fixedLayerObject) {
|
|
|
|
|
|
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸");
|
|
|
|
|
|
return this._exportWithCanvasSize(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
|
|
|
|
|
|
const fixedBounds = fixedLayerObject.getBoundingRect();
|
|
|
|
|
|
|
|
|
|
|
|
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
|
|
|
|
|
const canvasWidth = Math.round(fixedBounds.width);
|
|
|
|
|
|
const canvasHeight = Math.round(fixedBounds.height);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
|
|
|
|
|
console.log("固定图层边界:", fixedBounds);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建固定尺寸的临时画布
|
|
|
|
|
|
const scaleFactor = 2; // 高清导出
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const tempCanvas = document.createElement("canvas");
|
2025-06-18 11:05:23 +08:00
|
|
|
|
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,
|
|
|
|
|
|
enableRetinaScaling: true,
|
|
|
|
|
|
imageSmoothingEnabled: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
tempFabricCanvas.setZoom(1);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取图层下标为1的对象作为裁剪路径
|
|
|
|
|
|
const clipPathObject = await this._getLayerClipPathObject(1, fixedBounds);
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
|
|
|
|
|
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: cloned.left - fixedBounds.left,
|
|
|
|
|
|
top: cloned.top - fixedBounds.top,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新对象坐标
|
|
|
|
|
|
cloned.setCoords();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置裁剪路径到对象
|
|
|
|
|
|
if (clipPathObject) {
|
|
|
|
|
|
cloned.clipPath = clipPathObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tempFabricCanvas.add(cloned);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染画布
|
|
|
|
|
|
tempFabricCanvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
// 生成图片
|
|
|
|
|
|
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this._cleanupTempCanvas(tempFabricCanvas);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 普通模式导出(使用画布尺寸)
|
|
|
|
|
|
* @param {Array} objectsToExport 要导出的对象数组
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度为1
|
|
|
|
|
|
* @returns {String} 图片数据URL
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
async _exportWithCanvasSize(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 使用当前画布尺寸
|
|
|
|
|
|
const canvasWidth = this.canvas.width;
|
|
|
|
|
|
const canvasHeight = this.canvas.height;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建与画布相同尺寸的临时画布
|
|
|
|
|
|
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";
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
|
|
|
|
|
backgroundColor: null,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
tempFabricCanvas.enableRetinaScaling = true;
|
|
|
|
|
|
tempFabricCanvas.imageSmoothingEnabled = true;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
tempFabricCanvas.setZoom(scaleFactor);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 获取固定图层对象
|
|
|
|
|
|
* @returns {Object|null} 固定图层对象
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
_getFixedLayerObject() {
|
|
|
|
|
|
const allLayers = this._getAllLayers();
|
|
|
|
|
|
const fixedLayer = allLayers.find((layer) => layer.isFixed);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
if (!fixedLayer || !fixedLayer.fabricObject) {
|
|
|
|
|
|
return null;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 如果有ID,通过ID查找画布中的实际对象
|
|
|
|
|
|
if (fixedLayer.fabricObject.id) {
|
|
|
|
|
|
const result = findObjectById(this.canvas, fixedLayer.fabricObject.id);
|
|
|
|
|
|
return result.object || fixedLayer.fabricObject;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return fixedLayer.fabricObject;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 克隆对象用于导出
|
|
|
|
|
|
* @param {Object} obj fabric对象
|
|
|
|
|
|
* @param {Boolean} forceRestoreOpacity 是否强制恢复透明度为1
|
|
|
|
|
|
* @param {Boolean} removeClipPath 是否移除裁剪路径
|
|
|
|
|
|
* @returns {Promise<Object>} 克隆的对象
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
async _cloneObjectForExport(
|
|
|
|
|
|
obj,
|
|
|
|
|
|
forceRestoreOpacity = false,
|
|
|
|
|
|
removeClipPath = true
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (!obj) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
obj.clone(
|
|
|
|
|
|
(cloned) => {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resolve(cloned);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resolve(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
["id", "layerId", "layerName", "name"]
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn("克隆对象失败:", error);
|
|
|
|
|
|
resolve(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 生成高质量数据URL
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @param {HTMLCanvasElement} canvas 画布元素
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
|
|
|
|
|
* @returns {String} 数据URL
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-18 11:05:23 +08:00
|
|
|
|
_generateHighQualityDataURL(canvas, expPicType) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const format = expPicType.toLowerCase();
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
switch (format) {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
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":
|
2025-06-09 10:25:54 +08:00
|
|
|
|
default:
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// PNG使用最高质量,支持透明背景
|
|
|
|
|
|
return canvas.toDataURL("image/png", 1.0);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成空白图片
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清理临时画布资源
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 备用方法:直接从图层数组中查找
|
|
|
|
|
|
const allLayers = this._getAllLayers();
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return allLayers.find((layer) => layer.id === layerId) || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从图层收集对象
|
|
|
|
|
|
* @param {Object} layer 图层对象
|
|
|
|
|
|
* @returns {Array} 对象数组
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_collectObjectsFromLayer(layer) {
|
|
|
|
|
|
if (!layer || !layer.fabricObjects) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return layer.fabricObjects.filter((obj) => obj && obj.visible !== false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否应该包含该图层
|
|
|
|
|
|
* @param {Object} layer 图层对象
|
|
|
|
|
|
* @param {Boolean} isContainBg 是否包含背景图层
|
|
|
|
|
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
|
|
|
|
|
* @returns {Boolean} 是否包含
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
|
|
|
|
|
|
// 背景图层
|
|
|
|
|
|
if (layer.isBackground) {
|
|
|
|
|
|
return isContainBg;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 固定图层
|
|
|
|
|
|
if (layer.isFixed) {
|
|
|
|
|
|
return isContainFixed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 普通图层始终包含
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算对象组的边界
|
|
|
|
|
|
* @param {Array} objects 对象数组
|
|
|
|
|
|
* @returns {Object} 边界信息 {left, top, width, height}
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_calculateGroupBounds(objects) {
|
|
|
|
|
|
if (!objects || objects.length === 0) {
|
|
|
|
|
|
return { left: 0, top: 0, width: 100, height: 100 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let minX = Infinity;
|
|
|
|
|
|
let minY = Infinity;
|
|
|
|
|
|
let maxX = -Infinity;
|
|
|
|
|
|
let maxY = -Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
objects.forEach((obj) => {
|
|
|
|
|
|
if (!obj) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
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);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn("计算对象边界失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有有效边界,使用默认值
|
|
|
|
|
|
if (minX === Infinity || minY === Infinity) {
|
|
|
|
|
|
return { left: 0, top: 0, width: 100, height: 100 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
left: minX,
|
|
|
|
|
|
top: minY,
|
|
|
|
|
|
width: maxX - minX,
|
|
|
|
|
|
height: maxY - minY,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建高质量导出画布
|
|
|
|
|
|
* @param {Object} bounds 边界信息
|
|
|
|
|
|
* @param {Number} scaleFactor 缩放因子
|
|
|
|
|
|
* @returns {HTMLCanvasElement} 画布元素
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_createHighQualityExportCanvas(bounds, scaleFactor) {
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
|
|
|
|
|
|
|
|
// 设置画布的实际像素尺寸(用于高清导出)
|
|
|
|
|
|
canvas.width = bounds.width * scaleFactor;
|
|
|
|
|
|
canvas.height = bounds.height * scaleFactor;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置画布的显示尺寸(CSS尺寸)
|
|
|
|
|
|
canvas.style.width = bounds.width + "px";
|
|
|
|
|
|
canvas.style.height = bounds.height + "px";
|
|
|
|
|
|
|
|
|
|
|
|
// 启用高质量渲染
|
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
ctx.imageSmoothingEnabled = true;
|
|
|
|
|
|
ctx.imageSmoothingQuality = "high";
|
|
|
|
|
|
|
|
|
|
|
|
return canvas;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建高质量临时Fabric画布
|
|
|
|
|
|
* @param {HTMLCanvasElement} canvas 画布元素
|
|
|
|
|
|
* @param {Object} bounds 边界信息
|
|
|
|
|
|
* @param {Number} scaleFactor 缩放因子
|
|
|
|
|
|
* @returns {fabric.StaticCanvas} Fabric画布
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_createHighQualityTempFabricCanvas(canvas, bounds, scaleFactor) {
|
|
|
|
|
|
const tempFabricCanvas = new fabric.StaticCanvas(canvas, {
|
|
|
|
|
|
width: bounds.width,
|
|
|
|
|
|
height: bounds.height,
|
|
|
|
|
|
backgroundColor: null, // 透明背景
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 启用高清缩放和图像平滑
|
|
|
|
|
|
tempFabricCanvas.enableRetinaScaling = true;
|
|
|
|
|
|
tempFabricCanvas.imageSmoothingEnabled = true;
|
|
|
|
|
|
tempFabricCanvas.setZoom(scaleFactor);
|
|
|
|
|
|
|
|
|
|
|
|
return tempFabricCanvas;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 克隆对象并添加到临时画布(带偏移处理)
|
|
|
|
|
|
* @param {fabric.StaticCanvas} tempCanvas 临时画布
|
|
|
|
|
|
* @param {Object} obj 原始对象
|
|
|
|
|
|
* @param {Object} bounds 边界信息
|
|
|
|
|
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
|
|
|
|
|
* @returns {Promise<Object>} 克隆的对象
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _cloneAndAddObjectWithOffset(
|
|
|
|
|
|
tempCanvas,
|
|
|
|
|
|
obj,
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
isRedGreenMode,
|
|
|
|
|
|
restoreOpacityInRedGreen
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (!obj) return null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const cloned = await this._cloneObjectForExport(
|
|
|
|
|
|
obj,
|
|
|
|
|
|
isRedGreenMode && restoreOpacityInRedGreen
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (cloned) {
|
|
|
|
|
|
// 调整对象位置,减去边界偏移量,使对象在新画布中正确定位
|
|
|
|
|
|
cloned.set({
|
|
|
|
|
|
left: cloned.left - bounds.left,
|
|
|
|
|
|
top: cloned.top - bounds.top,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新对象坐标
|
|
|
|
|
|
cloned.setCoords();
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到临时画布
|
|
|
|
|
|
tempCanvas.add(cloned);
|
|
|
|
|
|
|
|
|
|
|
|
return cloned;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn("克隆并添加对象失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将对象作为组导出(高质量版本)
|
|
|
|
|
|
* @param {Array} objectsToExport 要导出的对象数组
|
|
|
|
|
|
* @param {String} expPicType 导出类型
|
|
|
|
|
|
* @param {Number} scaleFactor 缩放因子,用于高清导出
|
|
|
|
|
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
|
|
|
|
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
|
|
|
|
|
* @returns {Promise<String>} 图片数据URL
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _exportObjectsAsGroup(
|
|
|
|
|
|
objectsToExport,
|
|
|
|
|
|
expPicType,
|
|
|
|
|
|
scaleFactor = 2,
|
|
|
|
|
|
isRedGreenMode = false,
|
|
|
|
|
|
restoreOpacityInRedGreen = true
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (!objectsToExport || objectsToExport.length === 0) {
|
|
|
|
|
|
throw new Error("没有可导出的对象");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算所有对象的边界
|
|
|
|
|
|
const bounds = this._calculateGroupBounds(objectsToExport);
|
|
|
|
|
|
console.log("导出边界:", bounds);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建高质量临时画布
|
|
|
|
|
|
const tempCanvas = this._createHighQualityExportCanvas(bounds, scaleFactor);
|
|
|
|
|
|
const tempFabricCanvas = this._createHighQualityTempFabricCanvas(
|
|
|
|
|
|
tempCanvas,
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取图层下标为1的对象作为裁剪路径
|
|
|
|
|
|
* @param {Number} layerIndex 图层下标
|
|
|
|
|
|
* @param {Object} fixedBounds 固定图层边界
|
|
|
|
|
|
* @returns {Promise<Object|null>} 裁剪路径对象
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _getLayerClipPathObject(layerIndex, fixedBounds) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const allLayers = this._getAllLayers();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取指定下标的图层(从底到顶,下标0是最底层)
|
|
|
|
|
|
const targetLayerIndex = layerIndex;
|
|
|
|
|
|
|
|
|
|
|
|
if (targetLayerIndex < 0 || targetLayerIndex >= allLayers.length) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
`图层下标 ${layerIndex} 超出范围,总图层数: ${allLayers.length}`
|
|
|
|
|
|
);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const targetLayer = allLayers[targetLayerIndex];
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
!targetLayer ||
|
|
|
|
|
|
!targetLayer.visible ||
|
|
|
|
|
|
!targetLayer.fabricObjects ||
|
|
|
|
|
|
targetLayer.fabricObjects.length === 0
|
|
|
|
|
|
) {
|
|
|
|
|
|
console.warn(`图层下标 ${layerIndex} 不可见或没有对象`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取图层中的第一个对象作为裁剪路径
|
|
|
|
|
|
const clipObject = targetLayer.fabricObjects[0];
|
|
|
|
|
|
|
|
|
|
|
|
if (!clipObject) {
|
|
|
|
|
|
console.warn(`图层下标 ${layerIndex} 中没有可用的裁剪对象`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆对象作为裁剪路径
|
|
|
|
|
|
const clonedClipPath = await this._cloneObjectForExport(
|
|
|
|
|
|
clipObject,
|
|
|
|
|
|
false,
|
|
|
|
|
|
false
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!clonedClipPath) {
|
|
|
|
|
|
console.warn(`无法克隆图层下标 ${layerIndex} 的裁剪对象`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调整裁剪路径的位置相对于固定图层
|
|
|
|
|
|
clonedClipPath.set({
|
|
|
|
|
|
left: clonedClipPath.left - fixedBounds.left,
|
|
|
|
|
|
top: clonedClipPath.top - fixedBounds.top,
|
|
|
|
|
|
absolutePositioned: true, // 使用绝对定位
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新坐标
|
|
|
|
|
|
clonedClipPath.setCoords();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`成功创建图层下标 ${layerIndex} 的裁剪路径:`, {
|
|
|
|
|
|
objectType: clonedClipPath.type,
|
|
|
|
|
|
position: { left: clonedClipPath.left, top: clonedClipPath.top },
|
|
|
|
|
|
size: { width: clonedClipPath.width, height: clonedClipPath.height },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return clonedClipPath;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`获取图层下标 ${layerIndex} 裁剪路径失败:`, error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
}
|