合并画布

This commit is contained in:
X1627315083
2025-06-22 13:52:28 +08:00
parent fd6d61a44a
commit 584f6a7db0
47 changed files with 4540 additions and 1952 deletions

View File

@@ -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;
}
}
}