合并画布
This commit is contained in:
@@ -225,16 +225,106 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图层顺序收集对象(从底到顶)
|
||||
* 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象)
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {Array} 画布中的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsFromLayer(layer) {
|
||||
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.children && layer.children.length > 0) {
|
||||
for (const childLayer of layer.children) {
|
||||
const childObjects = this._collectObjectsFromLayer(childLayer);
|
||||
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 this._exportWithCanvasSize(
|
||||
[obj],
|
||||
expPicType,
|
||||
restoreOpacityInRedGreen
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图层顺序收集对象(优化版本 - 从底到顶)
|
||||
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @returns {Array} 按正确顺序排列的对象数组
|
||||
* @returns {Array} 按正确顺序排列的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
|
||||
const objectsToExport = [];
|
||||
const allLayers = this._getAllLayers();
|
||||
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
|
||||
|
||||
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
||||
for (let i = allLayers.length - 1; i >= 0; i--) {
|
||||
@@ -256,6 +346,126 @@ export class ExportManager {
|
||||
return objectsToExport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扁平化的图层列表(包含子图层)
|
||||
* @returns {Array} 扁平化的图层数组
|
||||
* @private
|
||||
*/
|
||||
_getAllLayersFlattened() {
|
||||
const flattenedLayers = [];
|
||||
const rootLayers = this._getAllLayers();
|
||||
|
||||
const flattenLayer = (layer) => {
|
||||
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<Object>} 克隆的对象
|
||||
* @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 要导出的对象数组
|
||||
@@ -270,7 +480,8 @@ export class ExportManager {
|
||||
restoreOpacityInRedGreen
|
||||
) {
|
||||
// 获取固定图层对象(衣服底图)作为参考
|
||||
const fixedLayerObject = this._getFixedLayerObject();
|
||||
const fixedLayerObject =
|
||||
this._getFixedLayerObject() ?? this.canvas.clipPath;
|
||||
if (!fixedLayerObject) {
|
||||
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸");
|
||||
return this._exportWithCanvasSize(
|
||||
@@ -281,7 +492,7 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
|
||||
const fixedBounds = fixedLayerObject.getBoundingRect();
|
||||
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
|
||||
|
||||
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
||||
const canvasWidth = Math.round(fixedBounds.width);
|
||||
@@ -308,8 +519,8 @@ export class ExportManager {
|
||||
tempFabricCanvas.setZoom(1);
|
||||
|
||||
try {
|
||||
// 获取图层下标为1的对象作为裁剪路径
|
||||
const clipPathObject = await this._getLayerClipPathObject(1, fixedBounds);
|
||||
// 获取裁剪路径对象(如果存在)
|
||||
const clipPathObject = await this._getClipPathObject(fixedBounds);
|
||||
|
||||
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
||||
for (let i = 0; i < objectsToExport.length; i++) {
|
||||
@@ -429,7 +640,39 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象用于导出
|
||||
* 异步克隆fabric对象(参照createRasterizedImage的方法)
|
||||
* @param {fabric.Object} obj 要克隆的对象
|
||||
* @param {Array} propertiesToInclude 要包含的属性
|
||||
* @returns {Promise<fabric.Object>} 克隆的对象
|
||||
* @private
|
||||
*/
|
||||
_cloneObjectAsync(
|
||||
obj,
|
||||
propertiesToInclude = ["id", "layerId", "layerName", "name"]
|
||||
) {
|
||||
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 是否移除裁剪路径
|
||||
@@ -443,41 +686,169 @@ export class ExportManager {
|
||||
) {
|
||||
if (!obj) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
obj.clone(
|
||||
(cloned) => {
|
||||
if (cloned) {
|
||||
// 保持原始位置和属性
|
||||
cloned.set({
|
||||
selectable: false,
|
||||
evented: false,
|
||||
visible: true,
|
||||
});
|
||||
try {
|
||||
// 使用异步克隆方法
|
||||
const cloned = await this._cloneObjectAsync(obj);
|
||||
|
||||
// 如果需要恢复透明度
|
||||
if (forceRestoreOpacity) {
|
||||
cloned.set({ opacity: 1 });
|
||||
}
|
||||
if (cloned) {
|
||||
// 保持原始位置和属性
|
||||
cloned.set({
|
||||
selectable: false,
|
||||
evented: false,
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// 移除裁剪路径以避免绝对路径问题
|
||||
if (removeClipPath && cloned.clipPath) {
|
||||
console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`);
|
||||
cloned.clipPath = null;
|
||||
}
|
||||
// 如果需要恢复透明度
|
||||
if (forceRestoreOpacity) {
|
||||
cloned.set({ opacity: 1 });
|
||||
}
|
||||
|
||||
resolve(cloned);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
["id", "layerId", "layerName", "name"]
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("克隆对象失败:", error);
|
||||
resolve(null);
|
||||
// 移除裁剪路径以避免绝对路径问题
|
||||
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<String>} 图片数据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<Object|null>} 裁剪路径对象
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -568,315 +939,27 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从图层收集对象
|
||||
* @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} 是否包含
|
||||
* @returns {Boolean} 是否应该包含
|
||||
* @private
|
||||
*/
|
||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
|
||||
// 背景图层
|
||||
if (!layer) return false;
|
||||
|
||||
// 检查背景图层
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user