feat: 添加对图层操作的支持,优化图层粘贴和变换命令,增强组图层遮罩位置更新逻辑

This commit is contained in:
bighuixiang
2025-07-16 18:08:20 +08:00
parent 5f29e488ed
commit 26581b234a
8 changed files with 409 additions and 164 deletions

View File

@@ -13,6 +13,7 @@ import {
removeCanvasObjectByObject,
} from "../utils/helper";
import { fabric } from "fabric-with-all";
import { restoreFabricObject } from "../utils/objectHelper";
/**
* 添加图层命令
@@ -188,12 +189,13 @@ export class PasteLayerCommand extends Command {
this.clipboardData = options.clipboardData;
this.layerManager = options.layerManager;
// 新图层相关属性
this.newLayer = null;
this.newLayerId = null;
this.newLayerId = generateId("layer_");
this.insertIndex = null;
this.oldActiveLayerId = null;
this.createdObjects = [];
this.isGroupLayer = false;
this.childLayerIdMap = new Map();
}
async execute() {
@@ -201,27 +203,9 @@ export class PasteLayerCommand extends Command {
console.error("剪贴板中没有图层数据");
return null;
}
const data = this.clipboardData;
const fabric = window.fabric;
if (!fabric) {
console.error("未找到fabric库");
return null;
}
// 生成新的图层ID
this.newLayerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
// 创建新图层
this.newLayer = {
...data,
id: this.newLayerId,
name: `${data.name} 副本`,
fabricObjects: [],
isCut: undefined,
serializedObjects: undefined,
};
const data = JSON.parse(JSON.stringify(this.clipboardData));
// 判断是否为组图层
this.isGroupLayer = data.type === "group" || !!data?.children?.length;
// 保存当前活动图层ID
this.oldActiveLayerId = this.activeLayerId.value;
@@ -229,144 +213,263 @@ export class PasteLayerCommand extends Command {
// 计算插入位置
this.insertIndex = this.layerManager._getInsertIndexAboveActiveLayer();
// 执行添加图层操作
if (this.insertIndex !== undefined && this.insertIndex !== null) {
this.layers.value.splice(this.insertIndex, 0, this.newLayer);
} else {
this.layers.value.push(this.newLayer);
}
if (this.isGroupLayer) {
// 粘贴为新的组图层递归生成子图层ID
const { groupLayer, allObjects } = await this._createGroupLayerFromClipboard(data);
if (groupLayer?.clippingMask) {
// 给遮罩蒙层也添加上偏移
groupLayer.clippingMask.left += 30;
groupLayer.clippingMask.top += 30;
}
this.newLayer = groupLayer;
this.newLayer.id = this.newLayerId;
// 更新活动图层
if (!this.newLayer.isBackground) {
// 插入组图层
if (this.insertIndex !== undefined && this.insertIndex !== null) {
this.layers.value.splice(this.insertIndex, 0, this.newLayer);
} else {
this.layers.value.push(this.newLayer);
}
// 重新创建遮罩对象
const clippingMaskFabricObject =
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) || null;
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
// 添加所有对象到画布
allObjects.forEach((obj) => {
obj.clipPath = clippingMaskFabricObject;
obj.dirty = true;
obj.setCoords();
this.canvas.add(obj);
});
this.createdObjects = allObjects;
// 设置活动图层为新组图层
this.activeLayerId.value = this.newLayer.id;
}
// 如果有序列化的对象,异步恢复它们
if (
data.serializedObjects &&
Array.isArray(data.serializedObjects) &&
data.serializedObjects.length > 0
) {
await this._restoreObjectsAsync(data);
} else {
this._onObjectsRestored(data);
// 普通图层粘贴逻辑
this.newLayer = {
...data,
id: this.newLayerId,
name: `${data.name} 副本`,
fabricObjects: [],
isCut: undefined,
serializedObjects: undefined,
};
if (this.insertIndex !== undefined && this.insertIndex !== null) {
this.layers.value.splice(this.insertIndex, 0, this.newLayer);
} else {
this.layers.value.push(this.newLayer);
}
if (!this.newLayer.isBackground) {
this.activeLayerId.value = this.newLayer.id;
}
if (
data.serializedObjects &&
Array.isArray(data.serializedObjects) &&
data.serializedObjects.length > 0
) {
await this._restoreObjectsAsync(data);
} else {
this._onObjectsRestored(data);
}
}
// 取消画布激活对象
this.canvas.discardActiveObject(); // 取消当前活动对象
// 重新排序
await this.layerManager?.sortLayersWithTool?.();
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity?.();
return this.newLayerId;
}
/**
* 异步恢复序列化的对象
* @param {Object} data 剪贴板数据
* @private
* 递归创建组图层及其所有子图层和对象
*/
async _restoreObjectsAsync(data) {
async _createGroupLayerFromClipboard(data) {
const groupId = generateId("group_layer_");
const childLayerIdMap = new Map();
const allObjects = [];
// 递归处理子图层
const processChildren = async (children) => {
const result = [];
for (const child of children) {
const newChildId = generateId("layer_");
childLayerIdMap.set(child.id, newChildId);
// 递归处理子组
let childLayer;
if (child.type === "group" && Array.isArray(child.children)) {
const { groupLayer, allObjects: childObjs } =
await this._createGroupLayerFromClipboard(child);
childLayer = groupLayer;
allObjects.push(...childObjs);
} else {
// 普通子图层
childLayer = {
...child,
id: newChildId,
name: `${child.name} 副本`,
fabricObjects: [],
isCut: undefined,
serializedObjects: undefined,
};
// 恢复对象
if (
child.serializedObjects &&
Array.isArray(child.serializedObjects) &&
child.serializedObjects.length > 0
) {
const objs = await this._enlivenObjects(
child.serializedObjects,
newChildId,
`${child.name} 副本`
);
childLayer.fabricObjects = objs.map((obj) =>
obj.toObject(["id", "layerId", "layerName"])
);
allObjects.push(...objs);
}
}
result.push(childLayer);
}
return result;
};
// 处理当前组的子图层
const children = await processChildren(data.children);
// 创建组图层对象
const groupLayer = {
...data,
id: groupId,
name: `${data.name} 副本`,
type: "group",
children,
fabricObjects: [],
isCut: undefined,
serializedObjects: undefined,
};
this.childLayerIdMap = childLayerIdMap;
// 取消画布激活对象
this.canvas.discardActiveObject(); // 取消当前活动对象
return { groupLayer, allObjects };
}
/**
* 恢复对象(用于普通图层和组图层的子图层)
*/
async _enlivenObjects(serializedObjects, layerId, layerName) {
return new Promise((resolve, reject) => {
fabric.util.enlivenObjects(data.serializedObjects, (objects) => {
fabric.util.enlivenObjects(serializedObjects, (objects) => {
try {
objects.forEach((obj) => {
// 生成新的对象ID
const newObjId = `obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
obj.id = newObjId;
obj.layerId = this.newLayerId;
obj.layerName = this.newLayer.name;
// 如果是复制操作,给对象添加偏移量
if (!data.isCut) {
const offset = 10;
if (obj.left !== undefined) obj.left += offset;
if (obj.top !== undefined) obj.top += offset;
}
// 添加到画布
this.canvas.add(obj);
// 添加到图层
this.newLayer.fabricObjects.push(obj);
// 记录创建的对象,用于撤销
this.createdObjects.push(obj);
obj.id = generateId("obj_");
obj.layerId = layerId;
obj.layerName = layerName;
// 添加偏移
const offset = 30;
if (obj.left !== undefined) obj.left += offset;
if (obj.top !== undefined) obj.top += offset;
});
this._onObjectsRestored(data);
resolve();
resolve(objects);
} catch (error) {
reject(error);
}
});
});
}
async _restoreObjectsAsync(data) {
return new Promise((resolve, reject) => {
fabric.util.enlivenObjects(data.serializedObjects, (objects) => {
try {
objects.forEach((obj) => {
const newObjId = generateId("obj_");
obj.id = newObjId;
obj.layerId = this.newLayerId;
obj.layerName = this.newLayer.name;
if (!data.isCut) {
const offset = 30;
if (obj.left !== undefined) obj.left += offset;
if (obj.top !== undefined) obj.top += offset;
}
this.canvas.add(obj);
this.newLayer.fabricObjects.push(obj.toObject(["id", "layerId", "layerName"]));
this.createdObjects.push(obj);
});
this._onObjectsRestored(data);
resolve();
} catch (error) {
console.error("恢复对象时发生错误:", error);
reject(error);
}
});
});
}
/**
* 对象恢复完成后的处理
* @param {Object} data 剪贴板数据
* @private
*/
_onObjectsRestored(data) {
// 更新对象交互性
this.layerManager?.updateLayersObjectsInteractivity?.();
// 重新排列对象
this.layerManager?._rearrangeObjects?.();
// 判断如果是剪切操作,粘贴完后需要删除剪贴板数据
if (data.isCut && this.layerManager) {
this.layerManager.clipboardData = null;
console.log(`已粘贴图层:${this.newLayer.name}(剪切)`);
} else {
console.log(`已粘贴图层:${this.newLayer.name}(复制)`);
}
// 重新渲染画布
if (this.canvas) {
this.canvas.renderAll();
}
}
undo() {
async undo() {
if (!this.newLayer || !this.newLayerId) return;
// 从图层列表删除该图层
const index = this.layers.value.findIndex((layer) => layer.id === this.newLayerId);
if (index !== -1) {
this.layers.value.splice(index, 1);
const { layer, parent } = findLayerRecursively(this.layers.value, this.newLayerId);
if (!layer) {
console.error(`图层 ${this.newLayerId} 不存在, 无法撤销`);
return false;
}
// 恢复原活动图层
this.activeLayerId.value = this.oldActiveLayerId;
if (parent) {
// 如果是子图层,直接从父图层中删除
const index = parent.children.findIndex((child) => child.id === this.newLayerId);
if (index !== -1) {
parent.children.splice(index, 1);
}
} else {
// 如果是顶级图层,直接从图层列表中删除
const index = this.layers.value.findIndex((l) => l.id === this.newLayerId);
if (index !== -1) {
this.layers.value.splice(index, 1);
}
}
// 从画布移除所有创建的对象
this.createdObjects.forEach((obj) => {
this.canvas.remove(obj);
this.createdObjects?.forEach((obj) => {
removeCanvasObjectByObject(this.canvas, obj);
});
// 如果图层有其他fabric对象也要移除
if (this.newLayer.fabricObjects && this.newLayer.fabricObjects.length > 0) {
this.newLayer.fabricObjects.forEach((obj) => {
if (!this.createdObjects.includes(obj)) {
this.canvas.remove(obj);
}
});
}
this.activeLayerId.value = this.oldActiveLayerId;
// 如果是剪切操作的撤销,需要恢复剪贴板数据
if (this.clipboardData && this.clipboardData.isCut && this.layerManager) {
this.layerManager.clipboardData = this.clipboardData;
}
// 取消激活对象
// 重新渲染画布
if (this.canvas) {
this.canvas.renderAll();
}
// 更新对象交互性
if (
this.layerManager &&
typeof this.layerManager.updateLayersObjectsInteractivity === "function"
) {
this.layerManager.updateLayersObjectsInteractivity();
}
this.canvas.discardActiveObject(); // 取消当前活动对象
// 重新排序
await this.layerManager?.sortLayersWithTool?.();
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity?.();
return true;
}
getInfo() {
@@ -375,6 +478,7 @@ export class PasteLayerCommand extends Command {
layerName: this.newLayer?.name || "未知图层",
layerId: this.newLayerId,
objectCount: this.createdObjects.length,
isGroupLayer: this.isGroupLayer,
};
}
}

View File

@@ -1,3 +1,6 @@
import { findObjectById } from "../utils/helper";
import { findLayerRecursively } from "../utils/layerHelper";
import { restoreFabricObject } from "../utils/objectHelper";
import { Command } from "./Command";
/**
@@ -18,6 +21,18 @@ export class TransformCommand extends Command {
this.initialState = options.initialState || null;
this.finalState = options.finalState || null;
this.objectType = options.objectType || "object";
this.layerManager = options.layerManager;
this.layers = options.layers || null;
this.lastSelectLayerId = options.lastSelectLayerId || null; // 最后选择的图层ID
const targetObject = findObjectById(this.canvas, this.objectId)?.object || null;
const { layer, parent } = findLayerRecursively(this.layers.value, targetObject?.layerId);
this.layer = layer;
this.parent = parent;
this.isSginleObject = parent?.id === this.lastSelectLayerId?.value; // 是否需要记录遮罩的变换位置 如果是组图层且组下只有一个图层有对象 且 用户最后点击的是父图层 则记录遮罩变更
}
/**
@@ -25,7 +40,7 @@ export class TransformCommand extends Command {
* 如果是首次执行,记录初始和最终状态
* 如果是重做,应用最终状态
*/
execute() {
async execute() {
if (!this.finalState) {
console.warn("没有最终状态可应用");
return false;
@@ -39,7 +54,7 @@ export class TransformCommand extends Command {
}
// 应用最终变换状态
this._applyTransform(targetObject, this.finalState);
await this._applyTransform(targetObject, this.finalState);
// 触发画布更新
this.canvas.renderAll();
@@ -51,7 +66,7 @@ export class TransformCommand extends Command {
* 撤销命令
* 应用初始状态
*/
undo() {
async undo() {
if (!this.initialState) {
console.warn("没有初始状态可恢复");
return false;
@@ -65,7 +80,7 @@ export class TransformCommand extends Command {
}
// 应用初始变换状态
this._applyTransform(targetObject, this.initialState);
await this._applyTransform(targetObject, this.initialState);
// 触发画布更新
this.canvas.renderAll();
@@ -86,8 +101,41 @@ export class TransformCommand extends Command {
* 应用变换状态到对象
* @private
*/
_applyTransform(object, transformState) {
async _applyTransform(object, transformState) {
if (!object || !transformState) return;
// 变换遮罩层 - 如果当前图层是组图层,且有遮罩并且组下只有一个图层有对象 则应用遮罩转换
if (
this.parent &&
this.parent?.clippingMask &&
this.parent?.children?.length === 1 &&
this.isSginleObject
) {
// 计算对象的变换位置
const moveLeft = object.left - transformState.left; // 计算移动的水平距离
const moveTop = object.top - transformState.top; // 计算移动的垂直距离
this.parent.clippingMask.left -= moveLeft;
this.parent.clippingMask.top -= moveTop;
// 重新创建遮罩对象
const clippingMaskFabricObject = await restoreFabricObject(
this.parent.clippingMask,
this.canvas
);
if (clippingMaskFabricObject) {
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
const clippingMask = this.parent.clippingMask;
object.clipPath = clippingMask;
}
}
// 应用变换属性,只设置真正变化的值
Object.entries(transformState).forEach(([key, value]) => {

View File

@@ -1,4 +1,4 @@
import { findLayerRecursively } from "../utils/layerHelper";
import { findLayerRecursively, isGroupLayer } from "../utils/layerHelper";
import { restoreFabricObject } from "../utils/objectHelper";
import { Command } from "./Command";
@@ -23,6 +23,8 @@ export class UpdateGroupMaskPositionCommand extends Command {
this.maskInitialTop = options.maskInitialTop || 0;
this.isExecuteRealtime = options.isExecuteRealtime || false;
this.activeSelection = this.canvas.getActiveObject() || {};
this.isSginleObject = options.isSginleObject || false; // 组内是否单个对象移动
this.target = options.target || null; // 当前操作的目标对象
this.isFristExecute = true;
@@ -42,17 +44,18 @@ export class UpdateGroupMaskPositionCommand extends Command {
};
// 收集当前选择的所有对象位置
this.originalObjectsPostion = this.activeSelection.getObjects().map((obj) => {
return {
left: obj.left || 0,
top: obj.top || 0,
id: obj.id,
};
});
this.originalObjectsPostion =
this.activeSelection.getObjects?.()?.map((obj) => {
return {
left: obj.left || 0,
top: obj.top || 0,
id: obj.id,
};
}) || this.target?.getBoundingRect?.(true, true);
this.originalSelectionPosition = {
left: this.activeSelection.left || 0,
top: this.activeSelection.top || 0,
left: this.isSginleObject ? this.target.left : this.activeSelection.left || 0,
top: this.isSginleObject ? this.target.top : this.activeSelection.top || 0,
};
console.log(
@@ -147,6 +150,10 @@ export class UpdateGroupMaskPositionCommand extends Command {
// 更新所有使用此遮罩的子图层对象(不需要等待)
this._updateChildObjectsClipPath(layer);
this.target.set({
isSginleObject: this.isSginleObject,
});
return true;
} catch (error) {
console.error("实时更新组图层遮罩位置失败:", error);
@@ -217,21 +224,41 @@ export class UpdateGroupMaskPositionCommand extends Command {
}
if (isUndo) {
this.activeSelection.set({
left: this.originalSelectionPosition.left - this.deltaX,
top: this.originalSelectionPosition.top - this.deltaY,
});
this.activeSelection.dirty = true;
this.activeSelection.setCoords();
if (this.isSginleObject) {
// 如果是单个对象移动,直接更新目标对象位置
// this.target.set({
// left: this.target.left - this.deltaX,
// top: this.target.top - this.deltaY,
// });
// this.target.dirty = true;
// this.target.setCoords();
} else {
this.activeSelection.set({
left: this.originalSelectionPosition.left - this.deltaX,
top: this.originalSelectionPosition.top - this.deltaY,
});
this.activeSelection.dirty = true;
this.activeSelection.setCoords();
}
}
if (isExecute && !this.isFristExecute) {
this.activeSelection.set({
left: this.activeSelection.left + this.deltaX,
top: this.activeSelection.top + this.deltaY,
});
this.activeSelection.dirty = true;
this.activeSelection.setCoords();
if (this.isSginleObject) {
// 如果是单个对象移动,直接更新目标对象位置
// this.target.set({
// left: this.target.left + this.deltaX,
// top: this.target.top + this.deltaY,
// });
// this.target.dirty = true;
// this.target.setCoords();
} else {
this.activeSelection.set({
left: this.activeSelection.left + this.deltaX,
top: this.activeSelection.top + this.deltaY,
});
this.activeSelection.dirty = true;
this.activeSelection.setCoords();
}
}
// 触发画布重新渲染