合并画布

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

@@ -66,11 +66,13 @@ export class CanvasManager {
// 初始化缩略图管理器
this.thumbnailManager = new ThumbnailManager(this.canvas, {
// 可以根据需求自定义选项
layerThumbSize: { width: 32, height: 32 },
elementThumbSize: { width: 32, height: 24 },
// layerThumbSize: { width: 32, height: 32 },
// elementThumbSize: { width: 32, height: 24 },
layers: this.layers,
});
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
// 设置画布辅助线
initAligningGuidelines(this.canvas);
@@ -110,6 +112,8 @@ export class CanvasManager {
activeLayer,
});
this.thumbnailManager?.generateLayerThumbnail(activeLayer.id);
// 返回true表示不要自动添加到画布因为我们已经通过图层管理器处理了
return true;
} else {
@@ -147,6 +151,10 @@ export class CanvasManager {
// 更新交互性
command &&
(await this.layerManager?.updateLayersObjectsInteractivity?.());
this.thumbnailManager?.generateLayerThumbnail(
this.layerManager?.activeLayerId?.value
);
});
}
@@ -340,9 +348,9 @@ export class CanvasManager {
* 重置视图变换,使元素回到原始位置
* @private
*/
_resetViewportTransform() {
_resetViewportTransform(zoom) {
// 保存当前缩放值
const currentZoom = this.canvas.getZoom();
const currentZoom = zoom ?? this.canvas.getZoom();
// 重置视图变换,但保留缩放级别
this.canvas.setViewportTransform([currentZoom, 0, 0, currentZoom, 0, 0]);
@@ -370,6 +378,14 @@ export class CanvasManager {
// 获取背景对象
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
this.canvas?.clipPath?.set?.({
left: this.width / 2,
top: this.height / 2,
originX: "center",
originY: "center",
});
this.canvas?.clipPath?.setCoords?.();
// 如果只有背景层或没有背景层,使用原有逻辑
if (!backgroundObject) {
console.warn("未找到背景层,使用默认居中逻辑");
@@ -462,9 +478,12 @@ export class CanvasManager {
setCanvasColor(color) {
this.backgroundColor = color;
this.canvas.setBackgroundColor(
color,
this.canvas.renderAll.bind(this.canvas)
// this.canvas.setBackgroundColor(
// color,
// this.canvas.renderAll.bind(this.canvas)
// );
this.thumbnailManager?.generateLayerThumbnail?.(
this.layers?.value.find((layer) => layer.isBackground)?.id
);
}
@@ -615,80 +634,15 @@ export class CanvasManager {
* @param {String} layerId 图层ID
*/
updateLayerThumbnail(layerId) {
if (!this.thumbnailManager || !layerId || !this.layers) return;
const layer = this.layers.value.find((l) => l.id === layerId);
if (layer) {
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
/**
* 更新指定元素图层的缩略图
* @param {String} elementId 元素ID
* @param {Object} fabricObject fabric对象
*/
updateElementThumbnail(elementId, fabricObject) {
if (this.eventManager) {
this.eventManager.updateElementThumbnail(elementId, fabricObject);
} else if (
this.thumbnailManager &&
elementId &&
fabricObject &&
this.layers
) {
// 查找对应的图层(现在元素就是图层)
const layer = this.layers.value.find(
(l) =>
l.id === elementId ||
(l.fabricObject && l.fabricObject.id === elementId)
);
if (layer) {
// 生成图层缩略图
this.thumbnailManager.generateLayerThumbnail(layer);
}
// 同时也维护元素缩略图,以保持向后兼容性
this.thumbnailManager.generateElementThumbnail(
{ id: elementId, type: fabricObject.type },
fabricObject
);
}
this.thumbnailManager?.generateLayerThumbnail?.(layerId);
}
/**
* 更新所有图层和元素的缩略图
*/
updateAllThumbnails() {
if (!this.thumbnailManager || !this.layers) return;
this.thumbnailManager.generateAllLayerThumbnails(this.layers.value);
// 为所有元素生成缩略图
this.layers.value.forEach((layer) => {
// 如果是分组图层,处理子图层
if (isGroupLayer(layer) && layer.children) {
layer.children.forEach((childLayerId) => {
const childLayer = this.layers.value.find(
(l) => l.id === childLayerId
);
if (childLayer && childLayer.fabricObject) {
this.thumbnailManager.generateElementThumbnail(
{ id: childLayer.id, type: childLayer.fabricObject.type },
childLayer.fabricObject
);
}
});
}
// 如果是元素图层,则直接生成缩略图
else if (layer.isElementLayer && layer.fabricObject) {
this.thumbnailManager.generateElementThumbnail(
{ id: layer.id, type: layer.fabricObject.type },
layer.fabricObject
);
}
});
this.thumbnailManager?.generateAllLayerThumbnails?.(this.layers.value);
}
/**
@@ -710,6 +664,7 @@ export class CanvasManager {
layerManager: this.layerManager,
imageUrl: imageUrl,
targetLayerType: options.targetLayerType || "fixed", // background/fixed
options: options,
});
command.undoable =
@@ -812,110 +767,72 @@ export class CanvasManager {
getJSON() {
// // 简化图层数据在loadJSON时要根据id恢复引用
// let tempLayers = this.layers ? this.layers.value : [];
// // 创建对象ID映射表用于快速查找
// tempLayers = tempLayers.map((layer) => {
// const newLayer = { ...layer };
// // 处理fabricObjects数组
// if (Array.isArray(layer.fabricObjects)) {
// newLayer.fabricObjects = layer.fabricObjects
// .map((item) => {
// if (!item) return null;
// // 确保对象有ID
// if (!item.id) {
// item.id = `obj_${Date.now()}_${Math.floor(
// Math.random() * 10000
// )}`;
// }
// const simplifyLayers = (layers) => {
// return layers.map((layer) => {
// if (layer?.children?.length) {
// layer.children = layer.children.map((child) => {
// return {
// id: item.id,
// type: item.type || "object", // 保存类型信息用于调试
// id: child.id,
// type: child.type,
// layerId: child.layerId,
// layerName: child.layerName,
// isBackground: child.isBackground,
// isLocked: child.isLocked,
// isVisible: child.isVisible,
// isFixed: child.isFixed,
// parentId: child.parentId,
// fabricObject: child.fabricObject
// ? {
// id: child.fabricObject.id,
// type: child.fabricObject.type,
// layerId: child.fabricObject.layerId,
// layerName: child.fabricObject.layerName,
// }
// : {},
// fabricObjects:
// child.fabricObjects?.map((obj) => ({
// id: obj.id,
// type: obj.type,
// layerId: obj.layerId,
// layerName: obj.layerName,
// })) || [],
// };
// })
// .filter((item) => item !== null);
// } else {
// newLayer.fabricObjects = [];
// }
// if (layer.clippingMask) {
// layer.clippingMask = {
// id: layer.clippingMask.id,
// };
// }
// // 处理单个fabricObject
// if (layer.fabricObject) {
// if (!layer.fabricObject.id) {
// layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
// Math.random() * 10000
// )}`;
// });
// }
// newLayer.fabricObject = {
// id: layer.fabricObject.id,
// type: layer.fabricObject.type || "object",
// return {
// id: layer.id,
// type: layer.type,
// layerId: layer.layerId,
// layerName: layer.layerName,
// isBackground: layer.isBackground,
// isLocked: layer.isLocked,
// isVisible: layer.isVisible,
// isFixed: layer.isFixed,
// parentId: layer.parentId,
// fabricObject: child.fabricObject
// ? {
// id: child.fabricObject.id,
// type: child.fabricObject.type,
// layerId: child.fabricObject.layerId,
// layerName: child.fabricObject.layerName,
// }
// : {},
// fabricObjects:
// child.fabricObjects?.map((obj) => ({
// id: obj.id,
// type: obj.type,
// layerId: obj.layerId,
// layerName: obj.layerName,
// })) || [],
// children: layer.children,
// };
// } else {
// newLayer.fabricObject = null;
// }
// // 处理子图层
// if (Array.isArray(layer.children)) {
// newLayer.children = layer.children.map((cItem) => {
// const newChild = { ...cItem };
// // 处理子图层的fabricObjects
// if (Array.isArray(cItem.fabricObjects)) {
// newChild.fabricObjects = cItem.fabricObjects
// .map((item) => {
// if (!item) return null;
// if (!item.id) {
// item.id = `obj_${Date.now()}_${Math.floor(
// Math.random() * 10000
// )}`;
// }
// return {
// id: item.id,
// type: item.type || "object",
// };
// })
// .filter((item) => item !== null);
// } else {
// newChild.fabricObjects = [];
// }
// // 处理子图层的fabricObject
// if (cItem.fabricObject) {
// if (!cItem.fabricObject.id) {
// cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
// Math.random() * 10000
// )}`;
// }
// newChild.fabricObject = {
// id: cItem.fabricObject.id,
// type: cItem.fabricObject.type || "object",
// };
// } else {
// newChild.fabricObject = null;
// }
// return newChild;
// });
// } else {
// newLayer.children = [];
// }
// return newLayer;
// });
// });
// };
try {
console.log(
"获取画布JSON数据...",
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
const simplifyLayersData = simplifyLayers(
JSON.parse(JSON.stringify(this.layers.value))
);
console.log("获取画布JSON数据...", simplifyLayersData);
return JSON.stringify({
canvas: this.canvas.toJSON([
"id",
@@ -931,15 +848,14 @@ export class CanvasManager {
"eraserable",
"erasable",
]),
layers: JSON.stringify(
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
), // 简化图层数据
layers: JSON.stringify(simplifyLayersData), // 简化图层数据
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
version: "1.0", // 添加版本信息
timestamp: new Date().toISOString(), // 添加时间戳
canvasWidth: this.canvasWidth.value,
canvasHeight: this.canvasHeight.value,
canvasColor: this.canvasColor.value,
activeLayerId: this.canvas.activeLayerId.value,
activeLayerId: this.layerManager?.activeLayerId?.value,
});
} catch (error) {
console.error("获取画布JSON失败:", error);
@@ -960,7 +876,7 @@ export class CanvasManager {
try {
const parsedJson = JSON.parse(json);
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
const tempLayers = JSON.parse(parsedJson?.layers) || [];
const canvasData = parsedJson?.canvas;
@@ -983,19 +899,43 @@ export class CanvasManager {
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
// 重置视图变换以确保元素位置正确
this._resetViewportTransform();
this._resetViewportTransform(1);
let canvasClipPath = null;
// 克隆当前裁剪路径
if (this.canvas?.clipPath) {
canvasClipPath = this.canvas?.clipPath;
}
// 清除当前画布内容
this.canvas.clear();
console.log("清除当前画布内容", canvasData);
delete canvasData.clipPath; // 删除当前裁剪路径
// 加载画布数据
this.canvas.loadFromJSON(canvasData, async () => {
await optimizeCanvasRendering(this.canvas, async () => {
// 清空重做栈
this.commandManager?.clear?.();
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
if (canvasClipPath) {
// canvasClipPath.set({
// absolutePositioned: true,
// });
this.canvas.clipPath = canvasClipPath;
// await new Promise((resolve) => {
// debugger;
// fabric.util.enlivenObjects([canvasClipPath], (clipPaths) => {
// if (clipPaths && clipPaths.length > 0) {
// resolve(clipPaths[0]);
// } else {
// resolve(null);
// }
// });
// });
// debugger;
}
try {
// 重置画布数据
this.setCanvasSize(this.canvas.width, this.canvas.height);
// 重新构建对象关系
restoreObjectLayerAssociations(
this.layers.value,
@@ -1010,8 +950,8 @@ export class CanvasManager {
console.log("图层关联验证结果:", isValidate);
this.canvas.activeLayerId.value =
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
this.layerManager.activeLayerId.value =
this.layers.value[0]?.id || parsedJson?.activeLayerId || null;
// // 如果检测到红绿图模式内容,进行缩放调整
// if (this.enabledRedGreenMode) {
@@ -1026,12 +966,11 @@ export class CanvasManager {
false
);
console.log(this.layerManager.layers.value);
debugger;
// 更新所有缩略图
setTimeout(() => {
this.updateAllThumbnails();
}, 100);
}, 500);
console.log("画布JSON数据加载完成");
resolve();

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

View File

@@ -44,10 +44,8 @@ import {
BackgroundSizeCommand,
BackgroundSizeWithScaleCommand,
} from "../commands/BackgroundCommands";
import {
RasterizeLayerCommand,
MergeGroupLayerCommand,
} from "../commands/GroupCommands";
import { MergeGroupLayerCommand } from "../commands/GroupCommands";
import { RasterizeLayerCommand } from "../commands/RasterizeLayerCommand";
// 导入图层排序相关类和混入
import {
@@ -59,8 +57,13 @@ import {
import CanvasConfig from "../config/canvasConfig";
import { isBoolean, template } from "lodash-es";
import { findObjectById, optimizeCanvasRendering } from "../utils/helper";
import {
findObjectById,
generateId,
optimizeCanvasRendering,
} from "../utils/helper";
import { message } from "ant-design-vue";
import { fabric } from "fabric-with-all";
/**
* 图层管理器 - 负责管理画布上的所有图层
@@ -168,17 +171,17 @@ export class LayerManager {
* 根据当前编辑模式和图层状态设置对象的交互属性
* @private
*/
updateLayersObjectsInteractivity(isUseOptimize = true) {
async updateLayersObjectsInteractivity(isUseOptimize = true) {
if (!this.canvas) return;
if (isUseOptimize) {
// 优化渲染 - 统一批处理 支持异步回调
optimizeCanvasRendering(this.canvas, () => {
await optimizeCanvasRendering(this.canvas, async () => {
// 应用图层交互规则
this._applyInteractionRules();
await this._applyInteractionRules();
});
} else {
// 直接应用图层交互规则
this._applyInteractionRules();
await this._applyInteractionRules();
}
// // 性能优化使用requestAnimationFrame
@@ -201,7 +204,7 @@ export class LayerManager {
// this.canvas.renderAll(); // 确保画布重新渲染 - 同步渲染
// });
}
_setObjectInteractivity(obj, layer, editorMode) {
async _setObjectInteractivity(obj, layer, editorMode) {
// 设置可见性
obj.visible = layer.visible;
@@ -281,12 +284,14 @@ export class LayerManager {
if (layer.blendMode) obj.globalCompositeOperation = layer.blendMode;
if (layer.clippingMask) {
const { object } = findObjectById(this.canvas, layer.clippingMask?.id);
obj.clipPath = object || null;
if (object) {
obj.clipPath = object;
}
}
}
// 私有方法:应用交互规则
_applyInteractionRules() {
async _applyInteractionRules() {
console.log("updateLayersObjectsInteractivity ===>", this.editorMode);
const objects = this.canvas.getObjects();
const editorMode = this.editorMode || CanvasConfig.defaultTool;
@@ -302,7 +307,7 @@ export class LayerManager {
});
// 批量更新对象
objects.forEach((obj) => {
objects.forEach(async (obj) => {
const layer = layerMap[obj.layerId];
if (!obj.layerId) {
@@ -316,15 +321,15 @@ export class LayerManager {
if (!layer) return;
// 设置一级图层对象的交互性
this._setObjectInteractivity(obj, layer, editorMode);
await this._setObjectInteractivity(obj, layer, editorMode);
// 设置子图层对象的交互性
layer?.childLayer?.forEach((childLayer) => {
layer?.childLayer?.forEach(async (childLayer) => {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
if (childObj) {
this._setObjectInteractivity(childObj, childLayer, editorMode);
await this._setObjectInteractivity(childObj, childLayer, editorMode);
}
});
});
@@ -336,9 +341,13 @@ export class LayerManager {
* @param {Object} options 额外选项
* @returns {string} 新创建的图层ID
*/
createLayer(name = null, type = LayerType.EMPTY, options = {}) {
async createLayer(name = null, type = LayerType.EMPTY, options = {}) {
// 生成唯一ID
const layerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const layerId =
options.id ||
options.layerId ||
generateId("layer_") ||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const layerIndex = this.layers.value.length;
// 计算插入位置如果没有指定insertIndex则根据当前选中图层决定插入位置
@@ -380,15 +389,15 @@ export class LayerManager {
command.undoable = options.undoable;
// 如果是第一个图层或者普通图层数量小于等于3设置为不可撤销
if (this.layers.value.length === 3 || normalLayersCount <= 1) {
if (this.layers.value.length === 3 || normalLayersCount < 1) {
command.undoable = false;
}
// 执行命令
if (this.commandManager) {
this.commandManager.execute(command);
await this.commandManager.execute(command);
} else {
command.execute();
await command.execute();
}
return layerId;
@@ -505,7 +514,7 @@ export class LayerManager {
/**
* 初始化图层,确保有背景层、固定图层和一个空白图层
*/
initializeLayers() {
async initializeLayers() {
// 如果没有任何图层,创建背景层、固定图层和一个空白图层
if (this.layers.value.length === 0) {
// 创建背景图层
@@ -515,7 +524,7 @@ export class LayerManager {
this.createFixedLayer();
// 创建一个空白图层(默认位于背景图层和固定图层之上)
this.createLayer("图层 1");
await this.createLayer("图层 1");
} else {
// 检查是否已有背景层
const hasBackgroundLayer = this.layers.value.some(
@@ -539,7 +548,7 @@ export class LayerManager {
);
if (!hasNormalLayer) {
this.createLayer("图层 1");
await this.createLayer("图层 1");
}
}
@@ -679,14 +688,26 @@ export class LayerManager {
* @return {Object} layer 图层对象
*/
getActiveLayer() {
const { layer: activeLayer } = findLayerRecursively(
this.layers.value,
this.activeLayerId.value
);
if (activeLayer) {
return activeLayer;
} else {
console.warn("没有活动图层");
if (!this.activeLayerId.value) {
console.warn(
"没有活动图层ID无法获取活动图层 ==== 默认设置第一个图层为活动图层"
);
this.activeLayerId.value = this.layers.value[0]?.id || null;
}
try {
const { layer: activeLayer } = findLayerRecursively(
this.layers.value,
this.activeLayerId.value
);
if (activeLayer) {
return activeLayer;
} else {
console.warn("没有活动图层");
return null;
}
} catch (error) {
console.error("获取活动图层失败:", error);
return null;
}
}
@@ -868,27 +889,21 @@ export class LayerManager {
* @param {string} layerId 图层ID
* @returns {boolean} 更新后的可见性状态
*/
toggleLayerVisibility(layerId) {
async toggleLayerVisibility(layerId) {
// 直接创建和执行命令
const command = new ToggleLayerVisibilityCommand({
canvas: this.canvas,
layers: this.layers,
layerId: layerId,
layerManager: this,
});
// 执行命令
if (this.commandManager) {
this.commandManager.execute(command);
return await this.commandManager.execute(command);
} else {
command.execute();
return await command.execute();
}
// 更新对象交互性
this.updateLayersObjectsInteractivity();
// 获取当前可见性
const layer = this.layers.value.find((layer) => layer.id === layerId);
return layer ? layer.visible : false;
}
/**
@@ -2029,7 +2044,7 @@ export class LayerManager {
* @param {Object} options 文本选项
* @returns {Object} 创建的文本对象
*/
createTextLayerWithObject(textObject, options = {}) {
async createTextLayerWithObject(textObject, options = {}) {
if (!this.canvas || !textObject) return null;
// 确保对象有ID
@@ -2038,7 +2053,7 @@ export class LayerManager {
// 创建文本图层
const layerName = options.name || "文本图层";
const layerId = this.createLayer(layerName, LayerType.TEXT, {
const layerId = await this.createLayer(layerName, LayerType.TEXT, {
layerProperties: {
text: options.text || textObject.text || "新文本",
fontFamily: options.fontFamily || textObject.fontFamily || "Arial",
@@ -2075,7 +2090,9 @@ export class LayerManager {
const layer = this.getLayerById(layerId);
if (layer) {
layer.fabricObjects = layer.fabricObjects || [];
layer.fabricObjects.push(textObject);
layer.fabricObjects.push(
textObject.toObject(["id", "layerId", "layerName"])
);
}
// 设置此图层为活动图层
@@ -2218,7 +2235,7 @@ export class LayerManager {
* @param {string} parentId 父图层ID
* @returns {boolean} 是否删除成功
*/
removeChildLayer(layerId, parentId) {
async removeChildLayer(layerId, parentId) {
// 直接创建和执行命令
const command = new RemoveChildLayerCommand({
canvas: this.canvas,
@@ -2231,9 +2248,9 @@ export class LayerManager {
// 执行命令
if (this.commandManager) {
return this.commandManager.execute(command);
return await this.commandManager.execute(command);
} else {
return command.execute();
return await command.execute();
}
}
@@ -2298,28 +2315,22 @@ export class LayerManager {
* @param {string} parentId 父图层ID
* @returns {boolean} 更新后的可见性状态
*/
toggleChildLayerVisibility(layerId, parentId) {
async toggleChildLayerVisibility(layerId, parentId) {
// 直接创建和执行命令
const command = new ToggleChildLayerVisibilityCommand({
canvas: this.canvas,
layers: this.layers,
layerId: layerId,
parentId: parentId,
layerManager: this,
});
// 执行命令
if (this.commandManager) {
this.commandManager.execute(command);
return await this.commandManager.execute(command);
} else {
command.execute();
return await command.execute();
}
// 更新对象交互性
this.updateLayersObjectsInteractivity();
// 获取当前可见性
const childLayer = this.findChildLayer(layerId, parentId);
return childLayer ? childLayer.visible : false;
}
// ==================== 红绿图模式相关操作 ====================
@@ -2782,7 +2793,12 @@ export class LayerManager {
}
// 查找目标图层
const targetLayer = this.getLayerById(targetLayerId);
// const targetLayer = this.getLayerById(targetLayerId);
const { layer: targetLayer } = findLayerRecursively(
this.layers.value,
targetLayerId
);
if (!targetLayer) {
console.error($t("图层不存在", { layerId: targetLayerId }));
return false;

View File

@@ -1,3 +1,7 @@
import { findObjectById } from "../utils/helper";
import { findLayerRecursively } from "../utils/layerHelper";
import { createRasterizedImage } from "../utils/rasterizedImage";
/**
* 缩略图管理器 - 负责生成和缓存图层和元素的预览缩略图
*/
@@ -6,12 +10,8 @@ export class ThumbnailManager {
this.canvas = canvas;
this.layers = options.layers || []; // 图层管理器
this.layerThumbSize = options.layerThumbSize || { width: 48, height: 48 };
this.elementThumbSize = options.elementThumbSize || {
width: 36,
height: 36,
};
this.layerThumbnails = new Map(); // 图层缩略图缓存
this.elementThumbnails = new Map(); // 元素缩略图缓存
this.defaultThumbnail =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // 1x1 透明图
}
@@ -20,279 +20,31 @@ export class ThumbnailManager {
* 生成图层缩略图
* @param {Object} layer 图层对象ID
*/
generateLayerThumbnail(layerId) {
// const layer = this?.layers.value?.find((layer) => layer.id === layerId);
// if (!layer) return;
// // 延迟执行避免阻塞UI
// requestAnimationFrame(() => {
// this._generateLayerThumbnailNow(layer);
// });
}
/**
* 立即生成图层缩略图
* @param {Object} layer 图层对象
* @private
*/
_generateLayerThumbnailNow(layer) {
if (
!layer ||
!this.canvas ||
!layer.fabricObjects ||
!layer.fabricObjects?.length
)
return;
try {
let thumbnail = null;
if (!layer.children?.length) {
// 如果是元素图层,直接生成元素缩略图
thumbnail = this._generateThumbnailFromObjects(
layer.fabricObjects,
this.layerThumbSize.width,
this.layerThumbSize.height
);
} else if (layer.type === "group" || layer.children?.length) {
const fabricObjects = layer.children.reduce((pre, next) => {
if (next.fabricObjects.length) {
pre.push(...next.fabricObjects);
}
return pre;
}, []);
// 如果是分组图层,合并所有子对象的缩略图
thumbnail = this._generateThumbnailFromObjects(
fabricObjects,
this.layerThumbSize.width,
this.layerThumbSize.height
);
}
// 保存到缩略图缓存
if (thumbnail) {
this.layerThumbnails.set(layer.id, thumbnail);
} else {
// 如果无法生成缩略图,使用默认缩略图
this.layerThumbnails.set(layer.id, this.defaultThumbnail);
}
} catch (error) {
console.error("生成图层缩略图出错:", error);
}
}
/**
* 生成元素缩略图
* @param {Object} element 元素对象
* @param {Object} fabricObject fabric对象
*/
generateElementThumbnail(element, fabricObject) {
if (!element || !element.id || !fabricObject) return;
async generateLayerThumbnail(layerId) {
const fabricObjects = this._collectLayersAndObjects(layerId);
// 延迟执行避免阻塞UI
requestAnimationFrame(() => {
try {
const thumbnail = this._generateThumbnailFromObject(
fabricObject,
this.elementThumbSize.width,
this.elementThumbSize.height
);
fabricObjects.length > 0 &&
requestAnimationFrame(async () => {
const base64 = await this._generateLayerThumbnailNow(fabricObjects);
this.layerThumbnails.set(layerId, base64);
try {
const { layer, parent } = findLayerRecursively(
this.layers.value,
layerId
);
if (layer) {
layer.thumbnailUrl = base64; // 更新图层对象的缩略图
}
if (thumbnail) {
this.elementThumbnails.set(element.id, thumbnail);
}
} catch (error) {
console.error("生成元素缩略图出错:", error);
}
});
}
/**
* 从fabric对象生成缩略图
* @param {Object} obj fabric对象
* @param {Number} width 缩略图宽度
* @param {Number} height 缩略图高度
* @returns {String} 缩略图数据URL
* @private
*/
_generateThumbnailFromObject(obj, width, height) {
if (!obj || !this.canvas) return null;
// 保存对象状态
const originalState = {
active: obj.active,
visible: obj.visible,
left: obj.left,
top: obj.top,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
opacity: obj.opacity,
};
// 临时修改对象状态
obj.set({
active: false,
visible: true,
opacity: 1,
});
// 创建临时画布
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext("2d");
// 获取对象边界
const bounds = obj.getBoundingRect();
// 绘制缩略图
try {
// 清空画布
tempCtx.clearRect(0, 0, width, height);
// 计算缩放比例
const scaleFactorX = width / bounds.width;
const scaleFactorY = height / bounds.height;
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
// 居中绘制
const centerX = width / 2;
const centerY = height / 2;
tempCtx.save();
tempCtx.translate(centerX, centerY);
tempCtx.scale(scaleFactor, scaleFactor);
tempCtx.translate(
-bounds.left - bounds.width / 2,
-bounds.top - bounds.height / 2
);
// 绘制对象
obj.render(tempCtx);
tempCtx.restore();
// 转换为数据URL
const dataUrl = tempCanvas.toDataURL("image/png");
// 恢复对象状态
obj.set(originalState);
return dataUrl;
} catch (error) {
console.error("绘制对象缩略图出错:", error);
// 恢复对象状态
obj.set(originalState);
return null;
}
}
/**
* 从多个fabric对象生成组合缩略图
* @param {Array} objects fabric对象数组
* @param {Number} width 缩略图宽度
* @param {Number} height 缩略图高度
* @returns {String} 缩略图数据URL
* @private
*/
_generateThumbnailFromObjects(objects, width, height) {
if (!objects || !objects.length || !this.canvas) return null;
// 创建临时画布
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext("2d");
// 计算所有对象的总边界
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
objects.forEach((obj) => {
if (!obj.visible) 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);
});
const groupWidth = maxX - minX;
const groupHeight = maxY - minY;
// 如果没有有效对象返回null
if (groupWidth <= 0 || groupHeight <= 0) return null;
// 保存对象状态
const originalStates = objects.map((obj) => ({
obj,
state: {
active: obj.active,
visible: obj.visible,
opacity: obj.opacity,
},
}));
// 临时修改对象状态
originalStates.forEach((item) => {
item.obj.set({
active: false,
visible: true,
opacity: 1,
});
});
// 绘制缩略图
try {
// 清空画布
tempCtx.clearRect(0, 0, width, height);
// 计算缩放比例
const scaleFactorX = width / groupWidth;
const scaleFactorY = height / groupHeight;
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
// 居中绘制
const centerX = width / 2;
const centerY = height / 2;
tempCtx.save();
tempCtx.translate(centerX, centerY);
tempCtx.scale(scaleFactor, scaleFactor);
tempCtx.translate(-(minX + groupWidth / 2), -(minY + groupHeight / 2));
// 按顺序绘制所有对象
objects.forEach((obj) => {
if (obj.visible) {
obj.render(tempCtx);
if (parent) {
// 如果是组图层,则同步更新父图层的缩略图
this.generateLayerThumbnail(parent.id);
}
} catch (error) {
console.error("生成图层缩略图时出错:", error);
}
});
tempCtx.restore();
// 转换为数据URL
const dataUrl = tempCanvas.toDataURL("image/png");
// 恢复对象状态
originalStates.forEach((item) => {
item.obj.set(item.state);
});
return dataUrl;
} catch (error) {
console.error("绘制组合缩略图出错:", error);
// 恢复对象状态
originalStates.forEach((item) => {
item.obj.set(item.state);
});
return null;
}
}
/**
@@ -303,13 +55,133 @@ export class ThumbnailManager {
if (!layers || !Array.isArray(layers)) return;
// 使用requestAnimationFrame批量生成避免阻塞主线程
requestAnimationFrame(() => {
layers.forEach((layer) => {
if (layer && layer.id) {
this._generateLayerThumbnailNow(layer);
layers.forEach((layer) => {
if (layer && layer.id) {
this.generateLayerThumbnail(layer.id);
if (layer.children && layer.children.length) {
this.generateLayerThumbnail(layer.id);
}
}
});
}
// 生成图片
async _generateLayerThumbnailNow(fabricObjects) {
if (!fabricObjects || fabricObjects.length === 0) {
console.warn("⚠️ 没有对象需要生成缩略图,返回默认缩略图");
return this.defaultThumbnail;
}
return await createRasterizedImage({
canvas: this.canvas, // 画布对象 必填
fabricObjects, // 要栅格化的对象列表 - 按顺序 必填
// maskObject = null, // 用于裁剪的对象 - 可选 // TODO: 后期看是否需要裁剪
trimWhitespace: true, // 是否裁剪空白区域
trimPadding: 2, // 裁剪边距
quality: 0.8, // 图像质量
format: "png", // 图像格式
scaleFactor: 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL: true, // 是否返回DataURL而不是fabric.Image对象
});
}
/**
* 收集要栅格化的图层和对象
* @private
*/
_collectLayersAndObjects(layerId) {
if (!layerId) {
console.warn("⚠️ 无效的图层ID无法收集对象");
return [];
}
const { layer } = findLayerRecursively(this.layers.value, layerId);
let layersToRasterize = [];
if (layer.children && layer.children.length > 0) {
// 组图层:收集自身和所有子图层
layersToRasterize = this._collectLayersToRasterize(layer);
} else {
// 普通图层:只收集自身
layersToRasterize = [layer];
}
// 收集所有图层的fabricObjects并按画布z-index顺序排序
const allCanvasObjects = this.canvas.getObjects();
const objectsWithZIndex = [];
layersToRasterize.forEach((layer) => {
if (layer.fabricObject) {
// 如果图层本身有fabricObject直接添加
const { object } = findObjectById(this.canvas, layer.fabricObject.id);
if (object) {
const zIndex = allCanvasObjects.indexOf(object);
objectsWithZIndex.push({
object: object,
zIndex: zIndex,
layerObj: layer.fabricObject,
});
}
}
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
layer.fabricObjects.forEach((layerObj) => {
if (layerObj && layerObj.id) {
const { object } = findObjectById(this.canvas, layerObj.id);
if (object) {
// 获取对象在画布中的z-index数组索引
const zIndex = allCanvasObjects.indexOf(object);
objectsWithZIndex.push({
object: object,
zIndex: zIndex,
layerObj: layerObj,
});
}
}
});
}
});
// 按z-index排序确保保持原有的渲染顺序
objectsWithZIndex.sort((a, b) => a.zIndex - b.zIndex);
// 提取排序后的对象
const objectsToRasterize = objectsWithZIndex.map((item) => item.object);
console.log(
`📊 收集到 ${layersToRasterize.length} 个图层,${objectsToRasterize.length} 个对象进行栅格化`
);
console.log(
"🔢 对象z-index顺序:",
objectsWithZIndex.map((item) => ({
id: item.object.id,
type: item.object.type,
zIndex: item.zIndex,
}))
);
return objectsToRasterize;
}
/**
* 收集要栅格化的图层(递归收集子图层)
* @param {Object} sourceLayer 源图层
* @returns {Array} 图层数组
* @private
*/
_collectLayersToRasterize(sourceLayer) {
const result = [sourceLayer];
// 如果是组图层,收集所有子图层
if (sourceLayer.children && sourceLayer.children.length > 0) {
sourceLayer.children.forEach((childLayer) => {
if (childLayer) {
result.push(...this._collectLayersToRasterize(childLayer));
}
});
});
}
return result;
}
/**
@@ -322,16 +194,6 @@ export class ThumbnailManager {
return this.layerThumbnails.get(layerId) || null;
}
/**
* 获取元素缩略图
* @param {String} elementId 元素ID
* @returns {String|null} 缩略图URL或null
*/
getElementThumbnail(elementId) {
if (!elementId) return null;
return this.elementThumbnails.get(elementId) || null;
}
/**
* 清除图层缩略图
* @param {String} layerId 图层ID
@@ -342,22 +204,11 @@ export class ThumbnailManager {
}
}
/**
* 清除元素缩略图
* @param {String} elementId 元素ID
*/
clearElementThumbnail(elementId) {
if (elementId && this.elementThumbnails.has(elementId)) {
this.elementThumbnails.delete(elementId);
}
}
/**
* 清除所有缩略图
*/
clearAllThumbnails() {
this.layerThumbnails.clear();
this.elementThumbnails.clear();
}
/**

View File

@@ -1,11 +1,12 @@
import { BrushStore } from "../store/BrushStore";
import { BrushManager } from "./brushes/brushManager";
import { ToolCommand } from "../commands/ToolCommands";
import { CreateTextCommand } from "../commands/TextCommands";
import { OperationType } from "../utils/layerHelper";
import CanvasConfig from "../config/canvasConfig";
import { fabric } from "fabric-with-all";
import { InitLiquifyToolCommand } from "../commands/LiquifyCommands";
import { RasterizeLayerCommand } from "../commands/GroupCommands";
import { RasterizeLayerCommand } from "../commands/RasterizeLayerCommand";
import { message, Modal } from "ant-design-vue";
import { h } from "vue";
@@ -958,9 +959,33 @@ export class ToolManager {
* @param {Number} y 文本位置y坐标
* @param {Object} options 文本选项
*/
createText(x, y, options = {}) {
async createText(x, y, options = {}) {
// 使用命令模式创建文本
if (!this.canvas || !this.layerManager) return null;
if (this.commandManager) {
const command = new CreateTextCommand({
canvas: this.canvas,
layerManager: this.layerManager,
x,
y,
textOptions: options,
});
// 执行命令
return await this.commandManager.execute(command);
} else {
// 如果没有命令管理器,直接调用原有方法(兼容性)
return await this._createTextDirect(x, y, options);
}
}
/**
* 直接创建文本的方法(用于向后兼容)
* @param {Number} x 文本位置x坐标
* @param {Number} y 文本位置y坐标
* @param {Object} options 文本选项
* @private
*/
_createTextDirect(x, y, options = {}) {
// 默认文本属性
const defaultOptions = {
text: "双击编辑文本",
@@ -992,7 +1017,6 @@ export class ToolManager {
// 创建文本图层并通过LayerManager添加到画布
this.layerManager.createTextLayerWithObject(textObj, textOptions);
this.canvas.renderAll();
return textObj;
}

View File

@@ -0,0 +1,106 @@
# fabric-with-erasing 库的 erasable 属性功能使用指南
## 库功能概述
`fabric-with-erasing` 库提供了强大的基于属性的擦除控制功能,无需手动实现复杂的图层检查逻辑。
## 核心功能
### 1. erasable 属性的三种模式
- **`true`** (默认): 对象可以被擦除
- **`false`**: 对象不能被擦除
- **`'deep'`**: 对于组合对象,可以对内部可擦除的子对象进行细粒度控制
### 2. 选择性擦除机制
库内置了选择性擦除机制:
- 橡皮擦会自动检测对象的 `erasable` 属性
- 只有 `erasable !== false` 的对象才会被擦除
- 支持复杂的嵌套对象结构
### 3. 反向擦除功能
- 设置 `brush.inverted = true` 可以实现"撤销擦除"效果
- 恢复已被擦除的内容
## 项目中的优化实现
### LayerManager 优化
```javascript
// 基于图层状态自动设置 erasable 属性
obj.erasable = isInActiveLayer && layer.visible && !layer.locked && !layer.isBackground;
```
**优势:**
- 只有活动图层、可见、非锁定、非背景的对象才可擦除
- 自动处理复杂的权限逻辑
- 性能优秀,无需手动遍历检查
### BrushManager 简化
```javascript
// 直接使用库的 EraserBrush
this.brush = new fabric.EraserBrush(this.canvas);
this.brush.inverted = this.options.inverted || false;
```
**优势:**
- 移除了复杂的手动图层检查逻辑
- 直接利用库的内置功能
- 支持反向擦除(恢复功能)
- 代码更简洁、更可靠
## 使用示例
### 基础用法
```javascript
// 设置对象不可擦除
fabricObject.erasable = false;
// 设置对象可擦除
fabricObject.erasable = true;
// 组合对象的深度擦除控制
group.erasable = 'deep';
```
### 高级用法
```javascript
// 启用反向擦除模式
eraserBrush.inverted = true;
// 监听擦除事件
canvas.on('erasing:start', () => {
console.log('开始擦除');
});
canvas.on('erasing:end', (e) => {
console.log('擦除完成', e.targets);
});
```
## 性能优势
1. **内置优化**: 库已经进行了性能优化,避免重复计算
2. **事件驱动**: 基于事件的架构,响应更快
3. **选择性渲染**: 只重新渲染需要更新的部分
4. **内存效率**: 合理的对象管理和清理机制
## 兼容性说明
- 完全兼容标准的 fabric.js API
- 新增的 `erasable` 属性不会影响现有功能
- 可以逐步迁移现有代码
## 建议
1. **简化现有实现**: 移除手动的图层检查逻辑,直接使用 `erasable` 属性
2. **利用内置事件**: 使用库提供的擦除事件进行状态管理
3. **测试反向擦除**: 尝试使用 `inverted` 属性实现撤销功能
4. **性能测试**: 在大量对象的场景下测试性能表现
通过这些优化,你的项目可以获得更好的性能和更简洁的代码结构。

View File

@@ -0,0 +1,280 @@
<!-- https://github.com/tennisonchan/fabric-brush?tab=readme-ov-file -->
<!-- eraser_brushhttps://unpkg.com/fabric@5.5.2/src/mixins/eraser_brush.mixin.js -->
# 笔刷系统使用指南
## 概述
这是一个基于插件架构的笔刷系统,允许轻松扩展和添加新的笔刷类型。整个系统由以下几个关键部分组成:
1. `BaseBrush` - 所有笔刷的基类
2. `BrushRegistry` - 笔刷注册表,用于管理所有笔刷
3. `BrushManager` - 笔刷管理器,处理笔刷的实例化和切换
4. `BrushStore` - 笔刷状态存储
## 如何添加新笔刷
添加新笔刷只需简单几步:
### 1. 创建新的笔刷类
最简单的方法是继承 `BaseBrush` 类。在 `types` 目录下创建你的笔刷文件:
```javascript
import { BaseBrush } from '../BaseBrush';
/**
* 我的自定义笔刷
*/
export class MyCustomBrush extends BaseBrush {
constructor(canvas, options = {}) {
super(canvas, {
id: 'my-custom-brush',
name: '我的笔刷',
description: '这是我自定义的笔刷',
category: '自定义笔刷',
...options
});
}
// 创建笔刷实例
create() {
// 创建底层fabric.js笔刷
this.brush = new fabric.PencilBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
// 配置笔刷
configure(brush, options = {}) {
// 设置基本属性
if (options.color) brush.color = options.color;
if (options.width !== undefined) brush.width = options.width;
if (options.opacity !== undefined) brush.opacity = options.opacity;
// 设置自定义属性
brush.strokeLineCap = 'round';
brush.strokeLineJoin = 'round';
// ...更多自定义设置
}
}
```
### 2. 注册笔刷
有两种方式注册笔刷:
#### 方式一使用BrushRegistry直接注册
```javascript
import { brushRegistry } from '../BrushRegistry';
import { MyCustomBrush } from './MyCustomBrush';
// 注册笔刷
brushRegistry.register('my-custom-brush', MyCustomBrush, {
name: '我的笔刷',
description: '这是我自定义的笔刷',
category: '自定义笔刷'
});
```
#### 方式二通过BrushManager注册
```javascript
import { BrushManager } from '../brushManager';
import { MyCustomBrush } from './MyCustomBrush';
// 获取BrushManager实例
const brushManager = new BrushManager({ canvas });
// 注册笔刷
brushManager.registerBrush('my-custom-brush', MyCustomBrush, {
name: '我的笔刷',
description: '这是我自定义的笔刷',
category: '自定义笔刷'
});
```
### 3. 使用笔刷
注册笔刷后,可以在应用中使用它:
```javascript
// 切换到你的自定义笔刷
brushManager.setBrushType('my-custom-brush');
```
## 笔刷生命周期
每个笔刷有以下生命周期方法:
1. `constructor` - 创建笔刷类实例
2. `create` - 创建底层fabric.js笔刷实例
3. `configure` - 配置笔刷属性
4. `onSelected` - 笔刷被选中时调用
5. `onDeselected` - 笔刷被取消选中时调用
6. `destroy` - 销毁笔刷实例释放资源
## 示例:创建具有独特行为的笔刷
这个例子创建了一个"脉冲笔刷",线条宽度会自动脉动变化:
```javascript
import { BaseBrush } from '../BaseBrush';
export class PulseBrush extends BaseBrush {
constructor(canvas, options = {}) {
super(canvas, {
id: 'pulse',
name: '脉动笔刷',
description: '线条宽度会自动脉动变化',
category: '特效笔刷',
...options
});
this.originalWidth = options.width || 5;
this.pulseRate = options.pulseRate || 0.1;
this.pulseAmount = options.pulseAmount || 3;
this.pulseTimer = null;
}
create() {
this.brush = new fabric.PencilBrush(this.canvas);
this.configure(this.brush, this.options);
// 覆盖鼠标按下方法,开始脉冲效果
const originalMouseDown = this.brush.onMouseDown;
this.brush.onMouseDown = (pointer, options) => {
this.startPulse();
return originalMouseDown.call(this.brush, pointer, options);
};
// 覆盖鼠标松开方法,停止脉冲效果
const originalMouseUp = this.brush.onMouseUp;
this.brush.onMouseUp = (options) => {
this.stopPulse();
return originalMouseUp.call(this.brush, options);
};
return this.brush;
}
configure(brush, options = {}) {
if (options.width !== undefined) {
this.originalWidth = options.width;
brush.width = this.originalWidth;
}
if (options.color) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
if (options.pulseRate !== undefined) {
this.pulseRate = options.pulseRate;
}
if (options.pulseAmount !== undefined) {
this.pulseAmount = options.pulseAmount;
}
}
startPulse() {
this.stopPulse();
let phase = 0;
this.pulseTimer = setInterval(() => {
phase += this.pulseRate;
const pulseFactor = Math.sin(phase) * this.pulseAmount;
if (this.brush) {
this.brush.width = Math.max(1, this.originalWidth + pulseFactor);
}
}, 50);
}
stopPulse() {
if (this.pulseTimer) {
clearInterval(this.pulseTimer);
this.pulseTimer = null;
}
}
onDeselected() {
this.stopPulse();
}
destroy() {
this.stopPulse();
super.destroy();
}
}
```
## 使用预设笔刷类型
系统内置了几种笔刷类型,可以直接使用:
- `pencil` - 基础铅笔笔刷
- `spray` - 喷枪笔刷
- `marker` - 马克笔笔刷
- `eraser` - 橡皮擦笔刷
- `texture` - 材质笔刷
- `watercolor` - 水彩笔刷
- `chalk` - 粉笔笔刷
## 高级功能
### 1. 使用分类组织笔刷
注册笔刷时可以指定类别便于在UI中分组展示
```javascript
brushRegistry.register('my-brush', MyBrushClass, {
category: '艺术笔刷'
});
```
### 2. 自定义笔刷参数
可以为笔刷添加特殊参数,例如:
```javascript
// 笔刷类中
setGlowIntensity(intensity) {
this.glowIntensity = intensity;
// 更新笔刷效果
}
// 使用时
const neonBrush = brushManager.setBrushType('neon');
if (neonBrush && typeof neonBrush.setGlowIntensity === 'function') {
neonBrush.setGlowIntensity(15);
}
```
## 常见问题
### 如何创建复杂的自定义笔刷效果?
对于复杂的效果,可以:
1. 覆盖fabric.js笔刷的关键方法`onMouseMove`
2. 使用自定义渲染器处理绘制
3. 结合Canvas API创建特殊效果
### 如何获取所有注册的笔刷?
```javascript
// 获取所有笔刷
const allBrushes = brushRegistry.getAllBrushes();
// 获取所有分类
const categories = brushRegistry.getCategories();
// 获取指定分类的笔刷
const artisticBrushes = brushRegistry.getBrushesByCategory('艺术笔刷');
```

View File

@@ -76,7 +76,7 @@ export class BrushManager {
category: "基础笔刷",
});
brushRegistry.register("fur", FurBrush, {
name: "Texture",
name: "Fur",
description: "使用纹理图片作为笔刷,支持缩放和透明度",
category: "基础笔刷",
});

View File

@@ -12,12 +12,11 @@
* - https://mrdoob.com/projects/harmony/
* - http://perfectionkills.com/exploring-canvas-drawing-techniques/
*/
import { fabric } from "fabric-with-all";
import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
(function (fabric) {
/**
* Trim a canvas. Returns the lezft-top coordinate where trimming began.
* Trim a canvas. Returns the left-top coordinate where trimming began.
* @param {canvas} canvas A canvas element to trim. This element will be trimmed (reference).
* @returns {Object} Left-top coordinate of trimmed area. Example: {x:65, y:104}
* @see: https://stackoverflow.com/a/22267731/3360038
@@ -1744,5 +1743,4 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
_render: function () {},
}); // End WebBrush
})(fabric);
// })(typeof fabric !== "undefined" ? fabric : require("fabric").fabric);
})(typeof fabric !== "undefined" ? fabric : require("fabric").fabric);

View File

@@ -398,10 +398,7 @@ export class CanvasEventManager {
// 延迟更新以确保对象完全添加
setTimeout(() => {
// 现在图层就是元素本身,直接更新元素的缩略图
this.thumbnailManager.generateLayerThumbnail(
e.target.layerId,
e.target
);
this.thumbnailManager.generateLayerThumbnail(e.target.layerId);
}, 300);
}
});
@@ -469,8 +466,6 @@ export class CanvasEventManager {
this.canvas.on("object:removed", (e) => {
if (this.thumbnailManager && e.target) {
if (e.target.id) {
this.thumbnailManager.clearElementThumbnail(e.target.id);
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50);
@@ -670,12 +665,6 @@ export class CanvasEventManager {
// 生成图层缩略图
this.thumbnailManager.generateLayerThumbnail(layer);
}
// 同时也维护元素缩略图,以保持向后兼容性
this.thumbnailManager.generateElementThumbnail(
{ id: elementId, type: fabricObject.type },
fabricObject
);
}
/**

View File

@@ -0,0 +1,727 @@
# CommandManager 命令管理器使用指南
## 📖 概述
CommandManager 是一个基于经典命令模式的撤销/重做系统,提供队列机制防止快速执行时命令丢失,支持复合命令进行批量操作,确保了高性能和稳定性。
## ✨ 核心特性
- **撤销/重做**: 基于经典命令模式的撤销重做功能
- **队列机制**: 防止快速执行多个命令时的命令丢失
- **串行化执行**: 确保命令按顺序执行,避免并发问题
- **Promise支持**: 完全支持异步命令
- **性能监控**: 可选的性能统计功能
- **复合命令**: 支持批量操作和命令组合
- **状态管理**: 实时状态监控和回调
## 🎯 复合命令详解
### 什么是复合命令?
复合命令是一组相关操作的集合这些操作作为一个整体执行支持命令间的值传递和条件执行。在Canvas编辑器中复合命令特别适用于
- 创建复杂组件(同时创建多个对象和图层)
- 批量修改属性
- 导入文件(可能涉及多个图层和对象的创建)
- 复制粘贴操作
### 复合命令的执行流程
```mermaid
graph TD
A[创建复合命令] --> B[添加子命令]
B --> C[执行复合命令]
C --> D[串行执行子命令]
D --> E{执行成功?}
E -->|是| F[记录已执行命令]
F --> G[继续下一个命令]
G --> D
E -->|否| H[回滚已执行命令]
H --> I[抛出错误]
C --> J[撤销复合命令]
J --> K[逆序撤销子命令]
```
## 🚀 快速开始
### 1. 基础批量操作
```javascript
import { CommandManager } from './CommandManager.js';
import { Command } from '../../commands/Command.js';
// 创建命令管理器
const commandManager = new CommandManager({
maxHistorySize: 50,
performanceManager: null
});
// 创建简单命令
class SetPropertyCommand extends Command {
constructor(target, property, newValue) {
super({ name: '设置属性', description: `设置${property}${newValue}` });
this.target = target;
this.property = property;
this.newValue = newValue;
this.oldValue = null;
}
execute() {
this.oldValue = this.target[this.property];
this.target[this.property] = this.newValue;
return { property: this.property, oldValue: this.oldValue, newValue: this.newValue };
}
undo() {
this.target[this.property] = this.oldValue;
return { property: this.property, restoredValue: this.oldValue };
}
}
// 使用批量操作
const obj = { x: 0, y: 0, color: 'black' };
// 方法1: 使用 executeBatch
await commandManager.executeBatch([
new SetPropertyCommand(obj, 'x', 100),
new SetPropertyCommand(obj, 'y', 200),
new SetPropertyCommand(obj, 'color', 'red')
], '批量设置对象属性');
console.log(obj); // { x: 100, y: 200, color: 'red' }
// 撤销时,所有属性会一次性恢复
await commandManager.undo();
console.log(obj); // { x: 0, y: 0, color: 'black' }
```
### 2. 创建自定义复合命令
```javascript
import { CompositeCommand } from '../../commands/Command.js';
// 支持值传递的复合命令
class CreateLayerWithObjectsCommand extends CompositeCommand {
constructor(layerName, objects) {
super([], { name: '创建图层并添加对象' });
this.layerName = layerName;
this.objects = objects;
this.layerId = null;
}
async execute() {
// 先创建图层
const createLayerCmd = new CreateLayerCommand(this.layerName);
this.layerId = await createLayerCmd.execute();
this.addCommand(createLayerCmd);
// 使用返回的 layerId 添加对象
for (const obj of this.objects) {
const addObjectCmd = new AddObjectCommand(obj, this.layerId);
await addObjectCmd.execute();
this.addCommand(addObjectCmd);
}
this.executedCommands = [...this.commands];
return { layerId: this.layerId, objectCount: this.objects.length };
}
}
// 使用自定义复合命令
const objects = [object1, object2, object3];
const command = new CreateLayerWithObjectsCommand('新图层', objects);
const result = await commandManager.execute(command);
console.log(`创建了图层 ${result.layerId},包含 ${result.objectCount} 个对象`);
```
## 🔧 高级功能
### 1. 条件执行的复合命令
```javascript
class ConditionalSetupCommand extends CompositeCommand {
constructor(config) {
super([], { name: '条件设置' });
this.config = config;
}
async execute() {
// 总是创建基础对象
const createCmd = new CreateObjectCommand(this.config.object);
await createCmd.execute();
this.addCommand(createCmd);
// 根据条件决定是否创建图层
if (this.config.shouldCreateLayer) {
const layerCmd = new CreateLayerCommand(this.config.layerName);
await layerCmd.execute();
this.addCommand(layerCmd);
}
// 根据条件应用样式
if (this.config.applyStyle) {
const styleCmd = new ApplyStyleCommand(this.config.style);
await styleCmd.execute();
this.addCommand(styleCmd);
}
this.executedCommands = [...this.commands];
return { commandCount: this.commands.length };
}
}
```
### 2. 错误处理和回滚
```javascript
class RobustBatchCommand extends CompositeCommand {
constructor(operations) {
super([], { name: '健壮的批量操作' });
this.operations = operations;
}
async execute() {
try {
for (const operation of this.operations) {
const command = new operation.CommandClass(...operation.args);
// 执行命令
await command.execute();
this.addCommand(command);
console.log(`✅ 操作成功: ${command.constructor.name}`);
}
this.executedCommands = [...this.commands];
return { success: true, executedCount: this.commands.length };
} catch (error) {
console.error('❌ 批量操作失败:', error);
// 自动回滚已执行的命令
await this._rollbackExecutedCommands();
throw error;
}
}
}
```
### 3. 状态监控
```javascript
// 设置状态变化回调
commandManager.setChangeCallback((state) => {
console.log('命令管理器状态:', {
canUndo: state.canUndo,
canRedo: state.canRedo,
undoCount: state.undoCount,
redoCount: state.redoCount,
isExecuting: state.isExecuting,
lastCommand: state.lastCommand
});
});
// 获取命令历史
const history = commandManager.getHistory();
console.log('撤销历史:', history.undoHistory);
console.log('重做历史:', history.redoHistory);
```
## 💡 实际应用场景
### 1. 复杂UI组件创建
```javascript
class CreateComplexCardCommand extends CompositeCommand {
constructor(canvas, config) {
super([], { name: '创建复杂卡片' });
this.canvas = canvas;
this.config = config;
this.createdObjects = [];
}
async execute() {
try {
// 创建背景
const background = await this._createBackground();
const bgCmd = new AddObjectCommand(background);
await bgCmd.execute();
this.addCommand(bgCmd);
// 创建头部
const header = await this._createHeader();
const headerCmd = new AddObjectCommand(header);
await headerCmd.execute();
this.addCommand(headerCmd);
// 创建内容区域
const content = await this._createContent();
const contentCmd = new AddObjectCommand(content);
await contentCmd.execute();
this.addCommand(contentCmd);
// 创建按钮
const buttons = await this._createButtons();
for (const button of buttons) {
const buttonCmd = new AddObjectCommand(button);
await buttonCmd.execute();
this.addCommand(buttonCmd);
}
this.executedCommands = [...this.commands];
return { success: true, objectCount: this.commands.length };
} catch (error) {
await this._rollbackExecutedCommands();
throw error;
}
}
async _createBackground() {
// 创建背景逻辑
return new fabric.Rect({
width: this.config.width,
height: this.config.height,
fill: this.config.backgroundColor
});
}
// ...existing code...
}
```
### 2. 批量导入处理
```javascript
class ImportMultipleImagesCommand extends CompositeCommand {
constructor(files, layerManager) {
super([], { name: `导入${files.length}个图片文件` });
this.files = files;
this.layerManager = layerManager;
this.importedLayers = [];
}
async execute() {
try {
for (const file of this.files) {
// 加载图片
const image = await this._loadImageFromFile(file);
// 创建图层
const createLayerCmd = new CreateLayerCommand(file.name);
const layerId = await createLayerCmd.execute();
this.addCommand(createLayerCmd);
// 添加对象到图层
const addObjectCmd = new AddObjectToLayerCommand(image, layerId);
await addObjectCmd.execute();
this.addCommand(addObjectCmd);
this.importedLayers.push(layerId);
}
this.executedCommands = [...this.commands];
return { importedLayers: this.importedLayers };
} catch (error) {
console.error('批量导入失败:', error);
await this._rollbackExecutedCommands();
throw error;
}
}
async _loadImageFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
fabric.Image.fromURL(e.target.result, resolve);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
}
// 使用
const files = [file1, file2, file3];
const importCmd = new ImportMultipleImagesCommand(files, layerManager);
const result = await commandManager.execute(importCmd);
console.log('导入的图层:', result.importedLayers);
```
### 3. 复制粘贴操作
```javascript
class CopyPasteObjectsCommand extends CompositeCommand {
constructor(sourceObjects, targetPosition) {
super([], { name: '复制粘贴对象' });
this.sourceObjects = sourceObjects;
this.targetPosition = targetPosition;
this.copiedObjects = [];
}
async execute() {
try {
for (const obj of this.sourceObjects) {
// 克隆对象
const clonedObj = await this._cloneObject(obj);
// 设置新位置
clonedObj.set({
left: this.targetPosition.x + (clonedObj.left - this.sourceObjects[0].left),
top: this.targetPosition.y + (clonedObj.top - this.sourceObjects[0].top)
});
// 添加到画布
const addCmd = new AddObjectCommand(clonedObj);
await addCmd.execute();
this.addCommand(addCmd);
this.copiedObjects.push(clonedObj);
}
this.executedCommands = [...this.commands];
return { copiedObjects: this.copiedObjects };
} catch (error) {
await this._rollbackExecutedCommands();
throw error;
}
}
async _cloneObject(obj) {
return new Promise((resolve) => {
obj.clone(resolve);
});
}
}
```
## 🛠️ 错误处理策略
### 1. 复合命令中的错误处理
```javascript
// 推荐的错误处理模式
class SafeBatchCommand extends CompositeCommand {
constructor(operations, options = {}) {
super([], { name: options.name || '安全批量操作' });
this.operations = operations;
this.stopOnError = options.stopOnError !== false; // 默认遇到错误就停止
}
async execute() {
const results = [];
const errors = [];
for (let i = 0; i < this.operations.length; i++) {
const operation = this.operations[i];
try {
const command = new operation.CommandClass(...operation.args);
const result = await command.execute();
this.addCommand(command);
results.push({ index: i, success: true, result });
console.log(`✅ 操作 ${i + 1}/${this.operations.length} 成功`);
} catch (error) {
errors.push({ index: i, error: error.message });
console.error(`❌ 操作 ${i + 1}/${this.operations.length} 失败:`, error);
if (this.stopOnError) {
// 回滚已执行的命令
await this._rollbackExecutedCommands();
throw new Error(`批量操作在第 ${i + 1} 个操作时失败: ${error.message}`);
}
}
}
this.executedCommands = [...this.commands];
return {
results,
errors,
successCount: results.length,
errorCount: errors.length
};
}
}
```
### 2. 超时处理
```javascript
class TimeLimitedCommand extends CompositeCommand {
constructor(operations, timeout = 5000) {
super([], { name: '限时批量操作' });
this.operations = operations;
this.timeout = timeout;
}
async execute() {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('操作超时')), this.timeout);
});
try {
const executePromise = this._executeOperations();
const result = await Promise.race([executePromise, timeoutPromise]);
this.executedCommands = [...this.commands];
return result;
} catch (error) {
await this._rollbackExecutedCommands();
throw error;
}
}
async _executeOperations() {
for (const operation of this.operations) {
const command = new operation.CommandClass(...operation.args);
await command.execute();
this.addCommand(command);
}
return { success: true, operationCount: this.operations.length };
}
}
```
## 📊 最佳实践
### 1. 命令粒度控制
```javascript
// ✅ 好的命令粒度 - 逻辑相关的操作组合
class CreateUserProfileCommand extends CompositeCommand {
constructor(userData) {
super([], { name: '创建用户档案' });
this.userData = userData;
}
async execute() {
const avatarCmd = new CreateAvatarCommand(this.userData.avatar);
await avatarCmd.execute();
this.addCommand(avatarCmd);
const nameCmd = new CreateNameLabelCommand(this.userData.name);
await nameCmd.execute();
this.addCommand(nameCmd);
const infoCmd = new CreateInfoPanelCommand(this.userData.info);
await infoCmd.execute();
this.addCommand(infoCmd);
this.executedCommands = [...this.commands];
return { success: true };
}
}
// ❌ 过大的命令粒度 - 不相关的操作混合
// 应该拆分成多个独立的命令
```
### 2. 命名规范
```javascript
// ✅ 清晰的命令命名
await commandManager.executeBatch([...], '上传并创建图片图层');
new CreateLayerWithObjectsCommand('新图层', objects);
new ImportMultipleImagesCommand(files, layerManager);
// ❌ 模糊的命令命名
await commandManager.executeBatch([...], '操作');
new BatchCommand();
new ProcessCommand();
```
### 3. 性能优化
```javascript
// 对于大量操作,考虑分批处理
class BatchProcessor {
constructor(commandManager, batchSize = 10) {
this.commandManager = commandManager;
this.batchSize = batchSize;
}
async processBatch(items, createCommand, progressCallback) {
const batches = this._createBatches(items, this.batchSize);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const commands = batch.map(item => createCommand(item));
await this.commandManager.executeBatch(
commands,
`批处理 ${i + 1}/${batches.length}`
);
// 更新进度
if (progressCallback) {
const progress = ((i + 1) / batches.length) * 100;
progressCallback(progress);
}
}
}
_createBatches(items, batchSize) {
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
}
// 使用示例
const processor = new BatchProcessor(commandManager, 10);
await processor.processBatch(
largeItemList,
(item) => new ProcessItemCommand(item),
(progress) => console.log(`处理进度: ${progress.toFixed(1)}%`)
);
```
## 🔧 API 参考
### CommandManager 核心方法
```javascript
// 执行单个命令
await commandManager.execute(command: Command): Promise<any>
// 批量执行命令
await commandManager.executeBatch(commands: Command[], batchName?: string): Promise<any>
// 撤销/重做
await commandManager.undo(): Promise<any>
await commandManager.redo(): Promise<any>
// 状态管理
commandManager.getState(): ManagerState
commandManager.getHistory(): HistoryInfo
commandManager.clear(): void
// 回调设置
commandManager.setChangeCallback(callback: (state: ManagerState) => void): void
```
### CompositeCommand 核心方法
```javascript
// 添加子命令
compositeCommand.addCommand(command: Command): CompositeCommand
compositeCommand.addCommands(commands: Command[]): CompositeCommand
// 执行和撤销
await compositeCommand.execute(): Promise<any>
await compositeCommand.undo(): Promise<any>
// 获取信息
compositeCommand.getInfo(): CommandInfo
```
## 🚨 常见问题
### Q1: 什么时候使用 executeBatch什么时候创建自定义复合命令
A:
- **使用 executeBatch**: 当命令之间没有数据依赖,只是简单的批量执行时
- **创建自定义复合命令**: 当需要命令间值传递、条件执行或复杂逻辑时
```javascript
// 简单批量 - 使用 executeBatch
await commandManager.executeBatch([
new SetPropertyCommand(obj, 'x', 100),
new SetPropertyCommand(obj, 'y', 200)
], '批量设置属性');
// 复杂逻辑 - 创建自定义复合命令
class CreateLayerWithObjectsCommand extends CompositeCommand {
// 需要使用前一个命令的返回值
}
```
### Q2: 复合命令执行失败时会自动回滚吗?
A: 是的,`CompositeCommand` 在执行失败时会自动回滚已执行的子命令。
```javascript
// 如果第3个命令失败前2个命令会被自动撤销
const commands = [command1, command2, failingCommand, command4];
try {
await commandManager.executeBatch(commands);
} catch (error) {
// 此时 command1 和 command2 已被自动撤销
}
```
### Q3: 如何处理部分命令失败的情况?
A: 创建自定义复合命令,设置错误处理策略:
```javascript
class TolerantBatchCommand extends CompositeCommand {
constructor(commands, options = {}) {
super([], { name: '容错批量命令' });
this.originalCommands = commands;
this.continueOnError = options.continueOnError || false;
}
async execute() {
const results = [];
for (const cmd of this.originalCommands) {
try {
await cmd.execute();
this.addCommand(cmd);
results.push({ success: true, command: cmd.constructor.name });
} catch (error) {
results.push({ success: false, error: error.message });
if (!this.continueOnError) {
await this._rollbackExecutedCommands();
throw error;
}
}
}
this.executedCommands = [...this.commands];
return results;
}
}
```
### Q4: 如何优化大量命令的性能?
A: 使用分批处理和进度反馈:
```javascript
// 分批处理大量命令
const BATCH_SIZE = 20;
const batches = [];
for (let i = 0; i < largeCommandList.length; i += BATCH_SIZE) {
batches.push(largeCommandList.slice(i, i + BATCH_SIZE));
}
for (const [index, batch] of batches.entries()) {
await commandManager.executeBatch(batch, `批次 ${index + 1}/${batches.length}`);
// 给UI更新的机会
await new Promise(resolve => setTimeout(resolve, 0));
}
```
## 🎯 总结
移除事务机制后的 CommandManager 更加简洁和灵活:
1. **简化架构**: 去除了不必要的事务状态管理
2. **更好的数据流**: 复合命令支持命令间值传递
3. **灵活的错误处理**: 自定义复合命令可以实现各种错误处理策略
4. **更清晰的API**: `executeBatch` 用于简单批量,自定义复合命令用于复杂逻辑
5. **更好的性能**: 减少了状态管理开销
推荐的使用模式:
- **简单批量操作**: 使用 `commandManager.executeBatch()`
- **复杂业务逻辑**: 创建继承自 `CompositeCommand` 的自定义命令
- **需要值传递**: 在自定义复合命令中处理命令间的数据依赖
- **错误处理**: 根据业务需求在复合命令中实现相应的错误处理策略