feat: 裁剪组裁剪跟随选择组移动

This commit is contained in:
bighuixiang
2025-07-14 01:00:23 +08:00
parent 96e13cb22a
commit 24e9ba8ae5
80 changed files with 2052 additions and 4292 deletions

View File

@@ -22,9 +22,7 @@ export class CreateBackgroundLayerCommand extends Command {
execute() {
// 检查是否已经存在背景图层
const existingBgLayer = this.layers.value.find(
(layer) => layer.isBackground
);
const existingBgLayer = this.layers.value.find((layer) => layer.isBackground);
if (existingBgLayer) {
console.warn("已存在背景层,不重复创建");
return existingBgLayer.id;
@@ -34,11 +32,7 @@ export class CreateBackgroundLayerCommand extends Command {
const bgObject = this._createBackgroundObject();
// 将背景对象添加到图层中
this.backgroundLayer.fabricObject = bgObject.toObject([
"id",
"layerId",
"type",
]);
this.backgroundLayer.fabricObject = bgObject.toObject(["id", "layerId", "type"]);
// 添加图层到最底部
this.layers.value.push(this.backgroundLayer);
@@ -54,9 +48,7 @@ export class CreateBackgroundLayerCommand extends Command {
undo() {
// 从图层列表中删除背景图层
const bgLayerIndex = this.layers.value.findIndex(
(layer) => layer.isBackground
);
const bgLayerIndex = this.layers.value.findIndex((layer) => layer.isBackground);
if (bgLayerIndex !== -1) {
this.layers.value.splice(bgLayerIndex, 1);
}
@@ -82,8 +74,7 @@ export class CreateBackgroundLayerCommand extends Command {
// 确保背景色为白色,如果没有设置或者是透明的话
const backgroundColor =
this.backgroundLayer.backgroundColor &&
this.backgroundLayer.backgroundColor !== "transparent"
this.backgroundLayer.backgroundColor && this.backgroundLayer.backgroundColor !== "transparent"
? this.backgroundLayer.backgroundColor
: "#ffffff";
@@ -139,10 +130,7 @@ export class UpdateBackgroundCommand extends Command {
// 查找背景图层
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
this.oldBackgroundColor = this.oldColor;
this.backgroundObject = findObjectById(
this.canvas,
this.bgLayer.fabricObject.id
).object;
this.backgroundObject = findObjectById(this.canvas, this.bgLayer.fabricObject.id).object;
}
execute() {
@@ -163,9 +151,7 @@ export class UpdateBackgroundCommand extends Command {
this.backgroundColorValue.value = this.backgroundColor; // 设置背景颜色
// 生成缩略图
this.canvasManager?.thumbnailManager?.generateLayerThumbnail?.(
this.bgLayer.id
);
this.canvasManager?.thumbnailManager?.generateLayerThumbnail?.(this.bgLayer.id);
return true;
}
@@ -186,9 +172,7 @@ export class UpdateBackgroundCommand extends Command {
this.backgroundColorValue.value = this.oldBackgroundColor; // 恢复背景颜色
// 生成缩略图
this.canvasManager?.thumbnailManager?.generateLayerThumbnail?.(
this.bgLayer.id
);
this.canvasManager?.thumbnailManager?.generateLayerThumbnail?.(this.bgLayer.id);
// 如果有旧颜色,恢复到旧颜色
return true;
}
@@ -223,10 +207,7 @@ export class BackgroundSizeCommand extends Command {
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
// 记录原尺寸
this.backgroundObject = findObjectById(
this.canvas,
this.bgLayer.fabricObject.id
).object;
this.backgroundObject = findObjectById(this.canvas, this.bgLayer.fabricObject.id).object;
this.oldWidth = this.backgroundObject.width;
this.oldHeight = this.backgroundObject.height;
@@ -255,8 +236,7 @@ export class BackgroundSizeCommand extends Command {
// 调整背景对象大小
if (this.bgLayer && this.backgroundObject) {
// 保持原有的背景颜色,如果没有设置则使用白色
const currentFill =
this.backgroundObject.fill || this.bgLayer.backgroundColor || "#ffffff";
const currentFill = this.backgroundObject.fill || this.bgLayer.backgroundColor || "#ffffff";
this.backgroundObject.set({
width: this.newWidth,
@@ -343,10 +323,7 @@ export class BackgroundSizeWithScaleCommand extends Command {
// 查找背景图层
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
this.backgroundObject = findObjectById(
this.canvas,
this.bgLayer.fabricObject.id
).object;
this.backgroundObject = findObjectById(this.canvas, this.bgLayer.fabricObject.id).object;
// 计算缩放比例
const scaleXRatio = this.newWidth / this.oldWidth;
@@ -409,24 +386,21 @@ export class BackgroundSizeWithScaleCommand extends Command {
// 统一缩放:使用平均值,保持相对比例的同时允许适度的形变
this.uniformScale = Math.sqrt(scaleXRatio * scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
this.offsetY = (this.newHeight - this.oldHeight * this.uniformScale) / 2;
break;
case "fit":
// 适应模式:使用较小值,确保所有内容都在画布内,可能有留白
this.uniformScale = Math.min(scaleXRatio, scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
this.offsetY = (this.newHeight - this.oldHeight * this.uniformScale) / 2;
break;
case "fill":
// 填充模式:使用较大值,填满画布,可能有部分内容被裁切
this.uniformScale = Math.max(scaleXRatio, scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
this.offsetY = (this.newHeight - this.oldHeight * this.uniformScale) / 2;
break;
case "stretch":
@@ -441,8 +415,7 @@ export class BackgroundSizeWithScaleCommand extends Command {
// 默认使用uniform模式
this.uniformScale = Math.sqrt(scaleXRatio * scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
this.offsetY = (this.newHeight - this.oldHeight * this.uniformScale) / 2;
}
}
@@ -462,8 +435,7 @@ export class BackgroundSizeWithScaleCommand extends Command {
// 调整背景对象大小和位置
if (this.bgLayer && this.backgroundObject) {
// 保持原有的背景颜色,如果没有设置则使用白色
const currentFill =
this.backgroundObject.fill || this.bgLayer.backgroundColor || "#ffffff";
const currentFill = this.backgroundObject.fill || this.bgLayer.backgroundColor || "#ffffff";
this.backgroundObject.set({
width: this.newWidth,
@@ -480,12 +452,10 @@ export class BackgroundSizeWithScaleCommand extends Command {
// 计算基于原始画布的缩放比例
const baseScaleX =
this.newWidth /
this.objectStates[0]?.obj._originalState?.baseCanvasWidth ||
this.newWidth / this.objectStates[0]?.obj._originalState?.baseCanvasWidth ||
this.newWidth / this.oldWidth;
const baseScaleY =
this.newHeight /
this.objectStates[0]?.obj._originalState?.baseCanvasHeight ||
this.newHeight / this.objectStates[0]?.obj._originalState?.baseCanvasHeight ||
this.newHeight / this.oldHeight;
// 根据策略缩放所有非背景对象
@@ -505,13 +475,11 @@ export class BackgroundSizeWithScaleCommand extends Command {
const baseUniformScale = Math.sqrt(baseScaleX * baseScaleY);
const baseOffsetX =
(this.newWidth -
(obj._originalState?.baseCanvasWidth || this.oldWidth) *
baseUniformScale) /
(obj._originalState?.baseCanvasWidth || this.oldWidth) * baseUniformScale) /
2;
const baseOffsetY =
(this.newHeight -
(obj._originalState?.baseCanvasHeight || this.oldHeight) *
baseUniformScale) /
(obj._originalState?.baseCanvasHeight || this.oldHeight) * baseUniformScale) /
2;
obj.set({
@@ -537,10 +505,7 @@ export class BackgroundSizeWithScaleCommand extends Command {
this.canvas.setHeight(this.oldHeight);
// 如果使用 CanvasManager通知它画布大小恢复
if (
this.canvasManager &&
typeof this.canvasManager.updateCanvasSize === "function"
) {
if (this.canvasManager && typeof this.canvasManager.updateCanvasSize === "function") {
this.canvasManager.updateCanvasSize(this.oldWidth, this.oldHeight);
}
@@ -568,12 +533,9 @@ export class BackgroundSizeWithScaleCommand extends Command {
const baseScaleX = this.oldWidth / originalState.baseCanvasWidth;
const baseScaleY = this.oldHeight / originalState.baseCanvasHeight;
const baseUniformScale = Math.sqrt(baseScaleX * baseScaleY);
const baseOffsetX =
(this.oldWidth - originalState.baseCanvasWidth * baseUniformScale) /
2;
const baseOffsetX = (this.oldWidth - originalState.baseCanvasWidth * baseUniformScale) / 2;
const baseOffsetY =
(this.oldHeight - originalState.baseCanvasHeight * baseUniformScale) /
2;
(this.oldHeight - originalState.baseCanvasHeight * baseUniformScale) / 2;
obj.set({
left: originalState.left * baseUniformScale + baseOffsetX,

View File

@@ -26,9 +26,7 @@ export class BrushSizeCommand extends BaseBrushCommand {
super({
...options,
name: `设置笔刷大小: ${options.size}`,
description: `将笔刷大小从 ${options.previousSize || "?"} 设为 ${
options.size
}`,
description: `将笔刷大小从 ${options.previousSize || "?"} 设为 ${options.size}`,
});
this.size = options.size;
@@ -66,9 +64,7 @@ export class BrushColorCommand extends BaseBrushCommand {
super({
...options,
name: `设置笔刷颜色: ${options.color}`,
description: `将笔刷颜色从 ${options.previousColor || "?"} 设为 ${
options.color
}`,
description: `将笔刷颜色从 ${options.previousColor || "?"} 设为 ${options.color}`,
});
this.color = options.color;
@@ -106,14 +102,11 @@ export class BrushOpacityCommand extends BaseBrushCommand {
super({
...options,
name: `设置笔刷透明度: ${options.opacity}`,
description: `将笔刷透明度从 ${options.previousOpacity || "?"} 设为 ${
options.opacity
}`,
description: `将笔刷透明度从 ${options.previousOpacity || "?"} 设为 ${options.opacity}`,
});
this.opacity = options.opacity;
this.previousOpacity =
options.previousOpacity || this.brushStore.state.opacity;
this.previousOpacity = options.previousOpacity || this.brushStore.state.opacity;
}
execute() {
@@ -147,9 +140,7 @@ export class BrushTypeCommand extends BaseBrushCommand {
super({
...options,
name: `设置笔刷类型: ${options.brushType}`,
description: `将笔刷类型从 ${options.previousType || "?"} 设为 ${
options.brushType
}`,
description: `将笔刷类型从 ${options.previousType || "?"} 设为 ${options.brushType}`,
});
this.brushType = options.brushType;
@@ -192,9 +183,7 @@ export class TextureCommand extends BaseBrushCommand {
super({
...options,
name: options.enabled ? "启用笔刷材质" : "禁用笔刷材质",
description: options.enabled
? `启用材质: ${options.path || "[默认]"}`
: "禁用笔刷材质",
description: options.enabled ? `启用材质: ${options.path || "[默认]"}` : "禁用笔刷材质",
});
this.enabled = options.enabled;
@@ -260,9 +249,7 @@ export class BrushPresetCommand extends BaseBrushCommand {
*/
constructor(options = {}) {
const presetName =
typeof options.preset === "object"
? options.preset.name
: `预设 ${options.preset}`;
typeof options.preset === "object" ? options.preset.name : `预设 ${options.preset}`;
super({
...options,
@@ -303,14 +290,10 @@ export class BrushPresetCommand extends BaseBrushCommand {
this.brushStore.applyPreset(this.preset);
} else if (typeof this.preset === "object") {
// 应用自定义预设对象
if (this.preset.size !== undefined)
this.brushStore.setBrushSize(this.preset.size);
if (this.preset.color !== undefined)
this.brushStore.setBrushColor(this.preset.color);
if (this.preset.opacity !== undefined)
this.brushStore.setBrushOpacity(this.preset.opacity);
if (this.preset.type !== undefined)
this.brushStore.setBrushType(this.preset.type);
if (this.preset.size !== undefined) this.brushStore.setBrushSize(this.preset.size);
if (this.preset.color !== undefined) this.brushStore.setBrushColor(this.preset.color);
if (this.preset.opacity !== undefined) this.brushStore.setBrushOpacity(this.preset.opacity);
if (this.preset.type !== undefined) this.brushStore.setBrushType(this.preset.type);
if (this.preset.textureEnabled !== undefined) {
this.brushStore.setTextureEnabled(this.preset.textureEnabled);
@@ -542,8 +525,7 @@ export class TexturePresetCommand extends BaseBrushCommand {
* @param {Object} options.brushManager 笔刷管理器实例
*/
constructor(options = {}) {
const presetName =
typeof options.preset === "object" ? options.preset.name : options.preset;
const presetName = typeof options.preset === "object" ? options.preset.name : options.preset;
super({
...options,
@@ -614,10 +596,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
});
this.file = options.file;
this.name =
options.name ||
options.file?.name?.replace(/\.[^/.]+$/, "") ||
"自定义纹理";
this.name = options.name || options.file?.name?.replace(/\.[^/.]+$/, "") || "自定义纹理";
this.category = options.category || "自定义材质";
this.texturePresetManager = options.texturePresetManager;
this.brushManager = options.brushManager;

View File

@@ -24,8 +24,7 @@ export class ClearSelectionContentCommand extends Command {
this.oldLayer = [...this.layers.value]; // 获取原图层对象
const { layer } =
findLayerRecursively(this.layers.value, this.targetLayerId) || {};
const { layer } = findLayerRecursively(this.layers.value, this.targetLayerId) || {};
// 栅格化相关属性
this.originalLayerBackup = null;
@@ -37,17 +36,10 @@ export class ClearSelectionContentCommand extends Command {
// this.originalLayerObject = fabric.util.object.clone(originalLayerObject); // 获取原图层对象
this.oldLayerObjects = originalLayerObject.toObject([
"id",
"layerId",
"layerName",
]); // 获取原图层的所有对象
this.oldLayerObjects = originalLayerObject.toObject(["id", "layerId", "layerName"]); // 获取原图层的所有对象
this.rasterizedImage = null;
this.targetLayer = findLayerRecursively(
this.canvas,
this.targetLayerId
)?.layer;
this.targetLayer = findLayerRecursively(this.canvas, this.targetLayerId)?.layer;
this.layerRasterized = false;
@@ -88,8 +80,7 @@ export class ClearSelectionContentCommand extends Command {
}
// 确定目标图层
const layerId =
this.targetLayerId || this.layerManager.getActiveLayerId();
const layerId = this.targetLayerId || this.layerManager.getActiveLayerId();
this.targetLayer = this.layerManager.getLayerById(layerId);
if (!this.targetLayer) {
console.error("无法清除选区内容:目标图层无效");
@@ -107,9 +98,7 @@ export class ClearSelectionContentCommand extends Command {
}
// 创建反转选区(保留选区外的内容)
const invertedSelection = await this._createInvertedSelection(
this.selectionObject
);
const invertedSelection = await this._createInvertedSelection(this.selectionObject);
if (!invertedSelection) {
console.error("创建反转选区失败");
return false;
@@ -137,10 +126,7 @@ export class ClearSelectionContentCommand extends Command {
// 恢复原图层状态
// 移除栅格化后的图像
if (
this.rasterizedImage &&
this.canvas.getObjects().includes(this.rasterizedImage)
) {
if (this.rasterizedImage && this.canvas.getObjects().includes(this.rasterizedImage)) {
this.canvas.remove(this.rasterizedImage);
}
@@ -199,10 +185,7 @@ export class ClearSelectionContentCommand extends Command {
let scaleFactor = this.baseResolutionScale;
if (this.highResolutionEnabled) {
const currentZoom = this.canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || this.canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.max(scaleFactor || this.canvas?.getRetinaScaling?.(), currentZoom);
scaleFactor = Math.min(scaleFactor, 3);
}
@@ -286,9 +269,7 @@ export class ClearSelectionContentCommand extends Command {
layerBounds.left + layerBounds.width
} ${layerBounds.top} L ${layerBounds.left + layerBounds.width} ${
layerBounds.top + layerBounds.height
} L ${layerBounds.left} ${layerBounds.top + layerBounds.height} Z ${
selectionObject.path
}`;
} L ${layerBounds.left} ${layerBounds.top + layerBounds.height} Z ${selectionObject.path}`;
const invertedPath = new fabric.Path(pathString, {
fillRule: "evenodd",
@@ -329,14 +310,8 @@ export class ClearSelectionContentCommand extends Command {
if (!bounds) {
bounds = { ...objBounds };
} else {
const right = Math.max(
bounds.left + bounds.width,
objBounds.left + objBounds.width
);
const bottom = Math.max(
bounds.top + bounds.height,
objBounds.top + objBounds.height
);
const right = Math.max(bounds.left + bounds.width, objBounds.left + objBounds.width);
const bottom = Math.max(bounds.top + bounds.height, objBounds.top + objBounds.height);
bounds.left = Math.min(bounds.left, objBounds.left);
bounds.top = Math.min(bounds.top, objBounds.top);
@@ -370,15 +345,10 @@ export class ClearSelectionContentCommand extends Command {
// 递归获取图层及其子图层的所有对象
const collectLayerObjects = (currentLayer) => {
// 处理图层的fabricObjects
if (
currentLayer.fabricObjects &&
Array.isArray(currentLayer.fabricObjects)
) {
if (currentLayer.fabricObjects && Array.isArray(currentLayer.fabricObjects)) {
currentLayer.fabricObjects.forEach((fabricObj) => {
if (fabricObj && fabricObj.id) {
const realObject = canvasObjects.find(
(canvasObj) => canvasObj.id === fabricObj.id
);
const realObject = canvasObjects.find((canvasObj) => canvasObj.id === fabricObj.id);
if (realObject && realObject.visible !== false) {
objects.push(realObject);
}
@@ -420,16 +390,12 @@ export class ClearSelectionContentCommand extends Command {
}
// 移除栅格化后的图像
if (
this.rasterizedImage &&
this.canvas.getObjects().includes(this.rasterizedImage)
) {
if (this.rasterizedImage && this.canvas.getObjects().includes(this.rasterizedImage)) {
this.canvas.remove(this.rasterizedImage);
}
// 恢复图层的fabricObjects
this.targetLayer.fabricObjects =
this.originalLayerBackup.fabricObjects || [];
this.targetLayer.fabricObjects = this.originalLayerBackup.fabricObjects || [];
// 重新创建并添加对象到画布
if (this.targetLayer.fabricObjects.length > 0) {

View File

@@ -109,10 +109,7 @@ export class CompositeCommand extends Command {
console.log(`✅ 子命令执行成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令执行失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令执行失败: ${command.constructor.name}`, error);
// 执行失败时,撤销已执行的命令
await this._rollbackExecutedCommands();
@@ -142,9 +139,7 @@ export class CompositeCommand extends Command {
return true;
}
console.log(
`↩️ 开始撤销复合命令,共 ${this.executedCommands.length} 个子命令`
);
console.log(`↩️ 开始撤销复合命令,共 ${this.executedCommands.length} 个子命令`);
try {
// 逆序撤销已执行的命令
@@ -164,10 +159,7 @@ export class CompositeCommand extends Command {
results.push(finalResult);
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令撤销失败: ${command.constructor.name}`, error);
// 撤销失败不中断整个撤销过程,但要记录错误
}
} else {
@@ -203,10 +195,7 @@ export class CompositeCommand extends Command {
}
console.log(`✅ 子命令回滚成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令回滚失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令回滚失败: ${command.constructor.name}`, error);
// 回滚失败不中断整个回滚过程
}
}
@@ -238,9 +227,7 @@ export class CompositeCommand extends Command {
commandCount: this.commands.length,
executedCount: this.executedCommands.length,
isExecuting: this.isExecuting,
subCommands: this.commands.map((cmd) =>
cmd.getInfo ? cmd.getInfo() : cmd.constructor.name
),
subCommands: this.commands.map((cmd) => (cmd.getInfo ? cmd.getInfo() : cmd.constructor.name)),
};
}
}

View File

@@ -85,9 +85,7 @@ export class CrossLevelMoveCommand extends Command {
this.layers.value.forEach((layer) => {
if (layer.fabricObject) {
states[layer.id] = {
isInCanvas: this.canvas
? this.canvas.contains(layer.fabricObject)
: false,
isInCanvas: this.canvas ? this.canvas.contains(layer.fabricObject) : false,
parentGroupId: null,
visible: layer.fabricObject.visible,
};
@@ -127,11 +125,7 @@ export class CrossLevelMoveCommand extends Command {
* 验证移动的有效性
*/
validateMove() {
const layer = this.findLayer(
this.layerId,
this.fromContainerType,
this.fromParentId
);
const layer = this.findLayer(this.layerId, this.fromContainerType, this.fromParentId);
if (!layer) {
throw new Error(`找不到要移动的图层: ${this.layerId}`);
@@ -153,9 +147,7 @@ export class CrossLevelMoveCommand extends Command {
// 检查目标父图层是否为组
if (this.toContainerType === "child") {
const targetParent = this.layers.value.find(
(layer) => layer.id === this.toParentId
);
const targetParent = this.layers.value.find((layer) => layer.id === this.toParentId);
if (!targetParent || !isGroupLayer(targetParent)) {
throw new Error("目标图层不是组图层");
}
@@ -198,15 +190,9 @@ export class CrossLevelMoveCommand extends Command {
try {
// 根据移动类型执行对应操作
if (
this.fromContainerType === "root" &&
this.toContainerType === "child"
) {
if (this.fromContainerType === "root" && this.toContainerType === "child") {
this.moveRootToGroup(draggedLayer);
} else if (
this.fromContainerType === "child" &&
this.toContainerType === "root"
) {
} else if (this.fromContainerType === "child" && this.toContainerType === "root") {
this.moveGroupToRoot(draggedLayer);
} else if (
this.fromContainerType === "child" &&
@@ -228,6 +214,17 @@ export class CrossLevelMoveCommand extends Command {
await this.layerManager?.updateLayersObjectsInteractivity();
this.canvas?.renderAll();
// 生成缩略图
this.fromParentId &&
this.layerManager?.canvasManager?.thumbnailManager?.generateLayerThumbnail?.(
this.fromParentId
);
this.toParentId &&
this.layerManager?.canvasManager?.thumbnailManager?.generateLayerThumbnail?.(
this.toParentId
);
console.log("✅ 跨层级移动命令执行成功");
return true;
} catch (error) {
@@ -266,7 +263,6 @@ export class CrossLevelMoveCommand extends Command {
this.restoreFabricObjectStates(this.beforeState.fabricObjects);
await this.layerManager?.updateLayersObjectsInteractivity();
this.canvas?.renderAll();
// 刷新画布
this.canvas?.renderAll();
console.log("✅ 跨层级移动命令撤销成功");
@@ -299,9 +295,7 @@ export class CrossLevelMoveCommand extends Command {
// 根据图层的parentId重新放置
if (layer.parentId) {
const parentLayer = this.layers.value.find(
(l) => l.id === layer.parentId
);
const parentLayer = this.layers.value.find((l) => l.id === layer.parentId);
if (
parentLayer &&
parentLayer.fabricObject &&
@@ -396,14 +390,9 @@ export class CrossLevelMoveCommand extends Command {
layer.children = layerData.children
.map((childData) => {
// 从原始图层数组中查找子图层
let childLayer = originalLayers.find(
(l) => l.id === childData.id
);
let childLayer = originalLayers.find((l) => l.id === childData.id);
if (!childLayer) {
childLayer = this.findLayerInOriginalStructure(
childData.id,
originalLayers
);
childLayer = this.findLayerInOriginalStructure(childData.id, originalLayers);
}
if (childLayer) {
// 更新子图层属性
@@ -465,9 +454,7 @@ export class CrossLevelMoveCommand extends Command {
// 根据目标状态放置对象
if (targetState.parentGroupId) {
// 应该在特定组中
const parentLayer = this.layers.value.find(
(l) => l.id === targetState.parentGroupId
);
const parentLayer = this.layers.value.find((l) => l.id === targetState.parentGroupId);
if (
parentLayer &&
parentLayer.fabricObject &&
@@ -532,12 +519,7 @@ export class CrossLevelMoveCommand extends Command {
*/
safeRemoveFromContainer(fabricObject, container, containerName) {
return this.safeFabricOperation(() => {
if (
container &&
container.remove &&
container.contains &&
container.contains(fabricObject)
) {
if (container && container.remove && container.contains && container.contains(fabricObject)) {
container.remove(fabricObject);
console.log(`✅ 成功从${containerName}移除对象`);
return true;
@@ -556,11 +538,7 @@ export class CrossLevelMoveCommand extends Command {
// 从所有组中移除
this.layers.value.forEach((layer) => {
if (layer.fabricObject && layer.fabricObject.type === "group") {
this.safeRemoveFromContainer(
fabricObject,
layer.fabricObject,
`${layer.id}`
);
this.safeRemoveFromContainer(fabricObject, layer.fabricObject, `${layer.id}`);
}
});
}
@@ -575,9 +553,7 @@ export class CrossLevelMoveCommand extends Command {
newIndex: this.newIndex,
});
const targetParent = this.layers.value.find(
(layer) => layer.id === this.toParentId
);
const targetParent = this.layers.value.find((layer) => layer.id === this.toParentId);
if (!targetParent) {
throw new Error(`找不到目标父图层: ${this.toParentId}`);
}
@@ -588,9 +564,7 @@ export class CrossLevelMoveCommand extends Command {
}
// 从顶级图层数组中移除
const rootIndex = this.layers.value.findIndex(
(layer) => layer.id === draggedLayer.id
);
const rootIndex = this.layers.value.findIndex((layer) => layer.id === draggedLayer.id);
if (rootIndex !== -1) {
this.layers.value.splice(rootIndex, 1);
console.log(`🗑️ 从顶级图层数组中移除图层 ${draggedLayer.id}`);
@@ -599,20 +573,14 @@ export class CrossLevelMoveCommand extends Command {
// 更新图层关系
draggedLayer.parentId = this.toParentId;
targetParent.children.splice(this.newIndex, 0, draggedLayer);
console.log(
`📂 将图层 ${draggedLayer.id} 添加到父图层 ${this.toParentId} 的children中`
);
console.log(`📂 将图层 ${draggedLayer.id} 添加到父图层 ${this.toParentId} 的children中`);
// 处理fabric对象的层级关系
if (draggedLayer.fabricObject && targetParent.fabricObject) {
console.log(`🎨 处理fabric对象层级关系`);
// 从画布中移除
this.safeRemoveFromContainer(
draggedLayer.fabricObject,
this.canvas,
"画布"
);
this.safeRemoveFromContainer(draggedLayer.fabricObject, this.canvas, "画布");
// 添加到父组
this.safeAddToContainer(
@@ -635,22 +603,16 @@ export class CrossLevelMoveCommand extends Command {
newIndex: this.newIndex,
});
const sourceParent = this.layers.value.find(
(layer) => layer.id === this.fromParentId
);
const sourceParent = this.layers.value.find((layer) => layer.id === this.fromParentId);
if (!sourceParent || !sourceParent.children) {
throw new Error(`找不到源父图层或其children数组: ${this.fromParentId}`);
}
// 从源父图层的children中移除
const childIndex = sourceParent.children.findIndex(
(child) => child.id === draggedLayer.id
);
const childIndex = sourceParent.children.findIndex((child) => child.id === draggedLayer.id);
if (childIndex !== -1) {
sourceParent.children.splice(childIndex, 1);
console.log(
`🗑️ 从父图层 ${this.fromParentId} 的children中移除图层 ${draggedLayer.id}`
);
console.log(`🗑️ 从父图层 ${this.fromParentId} 的children中移除图层 ${draggedLayer.id}`);
}
// 处理fabric对象的层级关系
@@ -675,9 +637,7 @@ export class CrossLevelMoveCommand extends Command {
// 计算在顶级图层中的插入位置
const targetIndex = Math.min(this.newIndex, this.layers.value.length);
this.layers.value.splice(targetIndex, 0, draggedLayer);
console.log(
`📂 将图层 ${draggedLayer.id} 添加到顶级图层数组位置 ${targetIndex}`
);
console.log(`📂 将图层 ${draggedLayer.id} 添加到顶级图层数组位置 ${targetIndex}`);
console.log("✅ 组内图层移动到顶级完成");
}
@@ -693,26 +653,18 @@ export class CrossLevelMoveCommand extends Command {
newIndex: this.newIndex,
});
const sourceParent = this.layers.value.find(
(layer) => layer.id === this.fromParentId
);
const targetParent = this.layers.value.find(
(layer) => layer.id === this.toParentId
);
const sourceParent = this.layers.value.find((layer) => layer.id === this.fromParentId);
const targetParent = this.layers.value.find((layer) => layer.id === this.toParentId);
if (!sourceParent || !targetParent) {
throw new Error("找不到源父图层或目标父图层");
}
// 从源父图层中移除
const childIndex = sourceParent.children.findIndex(
(child) => child.id === draggedLayer.id
);
const childIndex = sourceParent.children.findIndex((child) => child.id === draggedLayer.id);
if (childIndex !== -1) {
sourceParent.children.splice(childIndex, 1);
console.log(
`🗑️ 从源父图层 ${this.fromParentId} 中移除图层 ${draggedLayer.id}`
);
console.log(`🗑️ 从源父图层 ${this.fromParentId} 中移除图层 ${draggedLayer.id}`);
// 从源父组的fabricObject中移除
if (draggedLayer.fabricObject && sourceParent.fabricObject) {
@@ -726,9 +678,7 @@ export class CrossLevelMoveCommand extends Command {
// 更新图层的parentId
draggedLayer.parentId = this.toParentId;
console.log(
`🔗 更新图层 ${draggedLayer.id} 的parentId为 ${this.toParentId}`
);
console.log(`🔗 更新图层 ${draggedLayer.id} 的parentId为 ${this.toParentId}`);
// 确保目标父图层有children数组
if (!targetParent.children) {

View File

@@ -1,8 +1,4 @@
import {
createLayer,
findInChildLayers,
LayerType,
} from "../utils/layerHelper.js";
import { createLayer, findInChildLayers, LayerType } from "../utils/layerHelper.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand, Command } from "./Command.js";
import { CreateImageLayerCommand } from "./LayerCommands.js";
@@ -56,9 +52,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
const selectionObject = this.selectionManager.getSelectionObject();
if (selectionObject) {
try {
this._clonedSelectionObject = await this._cloneObject(
selectionObject
);
this._clonedSelectionObject = await this._cloneObject(selectionObject);
console.log("套索抠图:选区对象已克隆保存");
} catch (error) {
console.error("套索抠图:克隆选区对象失败:", error);
@@ -294,10 +288,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
await command.undo();
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令撤销失败: ${command.constructor.name}`, error);
// 子命令撤销失败不中断整个撤销过程
}
}
@@ -361,16 +352,11 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
// 递归获取图层及其子图层的所有对象
const collectLayerObjects = (currentLayer) => {
// 处理图层的fabricObjects
if (
currentLayer.fabricObjects &&
Array.isArray(currentLayer.fabricObjects)
) {
if (currentLayer.fabricObjects && Array.isArray(currentLayer.fabricObjects)) {
currentLayer.fabricObjects.forEach((fabricRef) => {
if (fabricRef && fabricRef.id) {
// 从画布中查找真实的对象
const realObject = canvasObjects.find(
(obj) => obj.id === fabricRef.id
);
const realObject = canvasObjects.find((obj) => obj.id === fabricRef.id);
if (realObject && realObject.visible) {
objects.push(realObject);
}
@@ -380,9 +366,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
// 处理单个fabricObject背景图层等
if (currentLayer.fabricObject && currentLayer.fabricObject.id) {
const realObject = canvasObjects.find(
(obj) => obj.id === currentLayer.fabricObject.id
);
const realObject = canvasObjects.find((obj) => obj.id === currentLayer.fabricObject.id);
if (realObject && realObject.visible) {
objects.push(realObject);
}
@@ -413,11 +397,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
) {
async _performCutoutWithRasterized(sourceObjects, selectionObject, selectionBounds) {
try {
console.log("=== 开始使用createRasterizedImage执行抠图 ===");
console.log(`源对象数量: ${sourceObjects.length}`);
@@ -456,10 +436,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
// 如果createRasterizedImage失败回退到原始方法
console.log("⚠️ 回退到原始抠图方法...");
return await this._performCutout(
{ fabricObjects: sourceObjects },
selectionObject
);
return await this._performCutout({ fabricObjects: sourceObjects }, selectionObject);
}
}
@@ -490,9 +467,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
try {
// 收集源图层中的可见对象
const visibleObjects = sourceLayer.fabricObjects.filter(
(obj) => obj.visible
);
const visibleObjects = sourceLayer.fabricObjects.filter((obj) => obj.visible);
if (visibleObjects.length === 0) {
throw new Error("源图层没有可见对象");
@@ -545,10 +520,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
let highResolutionScale = 1;
if (this.highResolutionEnabled) {
const devicePixelRatio = window.devicePixelRatio || 1;
highResolutionScale = Math.max(
this.baseResolutionScale,
devicePixelRatio * 2
);
highResolutionScale = Math.max(this.baseResolutionScale, devicePixelRatio * 2);
}
// 创建用于导出的高分辨率canvas
@@ -559,12 +531,8 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
colorSpace: "srgb",
});
const actualWidth = Math.round(
renderBounds.width * highResolutionScale
);
const actualHeight = Math.round(
renderBounds.height * highResolutionScale
);
const actualWidth = Math.round(renderBounds.width * highResolutionScale);
const actualHeight = Math.round(renderBounds.height * highResolutionScale);
exportCanvas.width = actualWidth;
exportCanvas.height = actualHeight;
@@ -773,17 +741,14 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
}
});
} else if (objectType === "polygon") {
fabric.Polygon.fromObject(
this.serializedSelectionObject,
(polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
fabric.Polygon.fromObject(this.serializedSelectionObject, (polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
);
});
} else if (objectType === "rect") {
fabric.Rect.fromObject(this.serializedSelectionObject, (rect) => {
if (rect) {
@@ -794,30 +759,24 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
}
});
} else if (objectType === "ellipse" || objectType === "circle") {
fabric.Ellipse.fromObject(
this.serializedSelectionObject,
(ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
fabric.Ellipse.fromObject(this.serializedSelectionObject, (ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
);
});
} else {
// 通用对象反序列化
fabric.util.enlivenObjects(
[this.serializedSelectionObject],
(objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
fabric.util.enlivenObjects([this.serializedSelectionObject], (objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
);
});
}
});
} catch (error) {

View File

@@ -1,8 +1,4 @@
import {
createLayer,
findInChildLayers,
LayerType,
} from "../utils/layerHelper.js";
import { createLayer, findInChildLayers, LayerType } from "../utils/layerHelper.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand, Command } from "./Command.js";
import { CreateImageLayerCommand } from "./LayerCommands.js";
@@ -59,9 +55,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
const selectionObject = this.selectionManager.getSelectionObject();
if (selectionObject) {
try {
this._clonedSelectionObject = await this._cloneObject(
selectionObject
);
this._clonedSelectionObject = await this._cloneObject(selectionObject);
console.log("剪切选区:选区对象已克隆保存");
} catch (error) {
console.error("剪切选区:克隆选区对象失败:", error);
@@ -132,11 +126,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
}
// 步骤2: 对原图层进行栅格化处理,移除选区内容
await this._rasterizeOriginalLayerWithCutout(
sourceLayer,
sourceObjects,
selectionObject
);
await this._rasterizeOriginalLayerWithCutout(sourceLayer, sourceObjects, selectionObject);
// 步骤3: 创建图像图层命令
const createImageLayerCmd = new CreateImageLayerCommand({
@@ -246,10 +236,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
}
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令撤销失败: ${command.constructor.name}`, error);
// 子命令撤销失败不中断整个撤销过程
}
}
@@ -313,17 +300,14 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
await this._restoreObjectsFromBackup(sourceLayer);
} else {
// 备用方案恢复原始的fabricObjects数组
sourceLayer.fabricObjects =
this.originalLayerBackup.fabricObjects || [];
sourceLayer.fabricObjects = this.originalLayerBackup.fabricObjects || [];
if (sourceLayer.fabricObjects.length > 0) {
await this._restoreLayerObjects(sourceLayer);
}
}
console.log(
`✅ 原图层状态恢复完成,恢复了 ${sourceLayer.fabricObjects.length} 个对象`
);
console.log(`✅ 原图层状态恢复完成,恢复了 ${sourceLayer.fabricObjects.length} 个对象`);
} catch (error) {
console.error("恢复原图层状态失败:", error);
throw error;
@@ -337,78 +321,68 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
*/
async _restoreObjectsFromBackup(layer) {
return new Promise((resolve, reject) => {
if (
!this.originalLayerObjectsData ||
this.originalLayerObjectsData.length === 0
) {
if (!this.originalLayerObjectsData || this.originalLayerObjectsData.length === 0) {
console.warn("没有对象备份数据");
resolve();
return;
}
try {
console.log(
`开始从备份恢复 ${this.originalLayerObjectsData.length} 个对象...`
);
console.log(`开始从备份恢复 ${this.originalLayerObjectsData.length} 个对象...`);
// 使用fabric.util.enlivenObjects重建对象
fabric.util.enlivenObjects(
this.originalLayerObjectsData,
(restoredObjects) => {
try {
let successCount = 0;
fabric.util.enlivenObjects(this.originalLayerObjectsData, (restoredObjects) => {
try {
let successCount = 0;
restoredObjects.forEach((obj, index) => {
if (obj) {
// 确保对象有正确的属性
obj.set({
layerId: layer.id,
layerName: layer.name,
selectable: true,
evented: true,
visible: true,
});
restoredObjects.forEach((obj, index) => {
if (obj) {
// 确保对象有正确的属性
obj.set({
layerId: layer.id,
layerName: layer.name,
selectable: true,
evented: true,
visible: true,
});
// 添加到画布
this.canvas.add(obj);
// 添加到画布
this.canvas.add(obj);
// 添加到图层的fabricObjects数组
layer.fabricObjects.push(
obj.toObject([
"id",
"layerId",
"layerName",
"parentId",
"selectable",
"evented",
"visible",
])
);
// 添加到图层的fabricObjects数组
layer.fabricObjects.push(
obj.toObject([
"id",
"layerId",
"layerName",
"parentId",
"selectable",
"evented",
"visible",
])
);
successCount++;
console.log(
`恢复对象 ${index + 1}/${restoredObjects.length}: ${
obj.id || obj.type
}`
);
} else {
console.warn(`对象 ${index + 1} 恢复失败`);
}
});
successCount++;
console.log(
`恢复对象 ${index + 1}/${restoredObjects.length}: ${obj.id || obj.type}`
);
} else {
console.warn(`对象 ${index + 1} 恢复失败`);
}
});
// 重新渲染画布
this.canvas.renderAll();
// 重新渲染画布
this.canvas.renderAll();
console.log(
`✅ 成功恢复了 ${successCount}/${this.originalLayerObjectsData.length} 个对象`
);
resolve();
} catch (error) {
console.error("处理恢复的对象时出错:", error);
reject(error);
}
console.log(
`✅ 成功恢复了 ${successCount}/${this.originalLayerObjectsData.length} 个对象`
);
resolve();
} catch (error) {
console.error("处理恢复的对象时出错:", error);
reject(error);
}
);
});
} catch (error) {
console.error("恢复对象时出错:", error);
reject(error);
@@ -474,11 +448,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
* @returns {fabric.Image} 抠图结果的fabric图像对象
* @private
*/
async _performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
) {
async _performCutoutWithRasterized(sourceObjects, selectionObject, selectionBounds) {
try {
console.log("=== 开始使用createRasterizedImage执行剪切抠图 ===");
console.log(`源对象数量: ${sourceObjects.length}`);
@@ -488,10 +458,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
let scaleFactor = this.baseResolutionScale;
if (this.highResolutionEnabled) {
const currentZoom = this.canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || this.canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.max(scaleFactor || this.canvas?.getRetinaScaling?.(), currentZoom);
scaleFactor = Math.min(scaleFactor, 3);
}
@@ -535,15 +502,10 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
// 递归获取图层及其子图层的所有对象
const collectLayerObjects = (currentLayer) => {
// 处理图层的fabricObjects
if (
currentLayer.fabricObjects &&
Array.isArray(currentLayer.fabricObjects)
) {
if (currentLayer.fabricObjects && Array.isArray(currentLayer.fabricObjects)) {
currentLayer.fabricObjects.forEach((fabricObj) => {
if (fabricObj && fabricObj.id) {
const realObject = canvasObjects.find(
(canvasObj) => canvasObj.id === fabricObj.id
);
const realObject = canvasObjects.find((canvasObj) => canvasObj.id === fabricObj.id);
if (realObject && realObject.visible !== false) {
objects.push(realObject);
}
@@ -649,17 +611,14 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
}
});
} else if (objectType === "polygon") {
fabric.Polygon.fromObject(
this.serializedSelectionObject,
(polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
fabric.Polygon.fromObject(this.serializedSelectionObject, (polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
);
});
} else if (objectType === "rect") {
fabric.Rect.fromObject(this.serializedSelectionObject, (rect) => {
if (rect) {
@@ -670,30 +629,24 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
}
});
} else if (objectType === "ellipse" || objectType === "circle") {
fabric.Ellipse.fromObject(
this.serializedSelectionObject,
(ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
fabric.Ellipse.fromObject(this.serializedSelectionObject, (ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
);
});
} else {
// 通用对象反序列化
fabric.util.enlivenObjects(
[this.serializedSelectionObject],
(objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
fabric.util.enlivenObjects([this.serializedSelectionObject], (objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
);
});
}
});
} catch (error) {
@@ -761,9 +714,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
this.originalLayerRasterized = false; // 标记原图层是否已栅格化
console.log(
`✅ 原图层状态已备份,对象数量: ${this.originalLayerObjectsData.length}`
);
console.log(`✅ 原图层状态已备份,对象数量: ${this.originalLayerObjectsData.length}`);
} catch (error) {
console.error("备份原图层状态失败:", error);
throw error;
@@ -777,11 +728,7 @@ export class CutSelectionToNewLayerCommand extends CompositeCommand {
* @param {Object} selectionObject 选区对象
* @private
*/
async _rasterizeOriginalLayerWithCutout(
sourceLayer,
sourceObjects,
selectionObject
) {
async _rasterizeOriginalLayerWithCutout(sourceLayer, sourceObjects, selectionObject) {
try {
console.log("=== 开始对原图层进行栅格化处理,移除选区内容 ===");

View File

@@ -92,9 +92,6 @@ export class EraserCommand extends Command {
if (!this.layerManager) return;
const canvasObjects = this.canvas.getObjects();
restoreObjectLayerAssociations(
this.layerManager?.layers?.value,
canvasObjects
);
restoreObjectLayerAssociations(this.layerManager?.layers?.value, canvasObjects);
}
}

View File

@@ -1,10 +1,5 @@
import { generateId } from "../utils/helper";
import {
createLayer,
findLayerRecursively,
LayerType,
OperationType,
} from "../utils/layerHelper";
import { createLayer, findLayerRecursively, LayerType, OperationType } from "../utils/layerHelper";
import { Command } from "./Command";
import {
findObjectById,
@@ -33,14 +28,12 @@ export class MergeGroupLayerCommand extends Command {
this.originalObjects = [...this.canvas.getObjects()];
this.flattenedLayer = null;
this.flattenedLayerId =
generateId("flattened_") ||
`flattened_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("flattened_") || `flattened_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.existingGroupId = null; // 用于查找现有组对象
// 组对象相关
this.newGroupId =
generateId("group") ||
`group_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("group") || `group_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
}
async execute() {
@@ -172,15 +165,11 @@ export class MergeGroupLayerCommand extends Command {
});
// 获取源图层在数组中的索引
const sourceIndex = this.layers.value.findIndex(
(l) => l.id === this.layerId
);
const sourceIndex = this.layers.value.findIndex((l) => l.id === this.layerId);
// 移除所有相关图层
const layerIdsToRemove = layersToFlatten.map((layer) => layer.id);
this.layers.value = this.layers.value.filter(
(layer) => !layerIdsToRemove.includes(layer.id)
);
this.layers.value = this.layers.value.filter((layer) => !layerIdsToRemove.includes(layer.id));
// 在原位置插入展平后的图层
this.layers.value.splice(sourceIndex, 0, this.flattenedLayer);
@@ -202,11 +191,7 @@ export class MergeGroupLayerCommand extends Command {
}
async undo() {
if (
!this.flattenedLayer ||
!this.originalLayers ||
!this.originalObjectStates
) {
if (!this.flattenedLayer || !this.originalLayers || !this.originalObjectStates) {
console.warn("没有可撤销的数据");
return;
}
@@ -324,10 +309,7 @@ export class MergeGroupLayerCommand extends Command {
groupObjects.forEach((obj) => {
try {
// 计算对象的绝对位置
const absolutePosition = this._calculateObjectAbsolutePosition(
obj,
groupObject
);
const absolutePosition = this._calculateObjectAbsolutePosition(obj, groupObject);
// 使用removeWithUpdate移除对象这会自动恢复对象的独立状态
groupObject.removeWithUpdate(obj);
@@ -382,10 +364,7 @@ export class MergeGroupLayerCommand extends Command {
const objectPoint = new fabric.Point(obj.left || 0, obj.top || 0);
// 应用组的变换矩阵计算绝对位置
const absolutePoint = fabric.util.transformPoint(
objectPoint,
groupTransform
);
const absolutePoint = fabric.util.transformPoint(objectPoint, groupTransform);
// 计算缩放比例
const totalScaleX = (group.scaleX || 1) * (obj.scaleX || 1);
@@ -483,9 +462,7 @@ export class MergeGroupLayerCommand extends Command {
canvasObj.layerId = layer.id;
canvasObj.layerName = layer.name;
console.log(
`🔗 恢复对象 ${canvasObj.id} 与图层 ${layer.name} 的关联`
);
console.log(`🔗 恢复对象 ${canvasObj.id} 与图层 ${layer.name} 的关联`);
}
}
});

View File

@@ -1,14 +1,7 @@
import {
createLayer,
findInChildLayers,
LayerType,
} from "../utils/layerHelper.js";
import { createLayer, findInChildLayers, LayerType } from "../utils/layerHelper.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand, Command } from "./Command.js";
import {
CreateImageLayerCommand,
RemoveLayerCommand,
} from "./LayerCommands.js";
import { CreateImageLayerCommand, RemoveLayerCommand } from "./LayerCommands.js";
import { fabric } from "fabric-with-all";
import { generateId } from "../utils/helper.js";
@@ -65,9 +58,7 @@ export class LassoCutoutCommand extends CompositeCommand {
const selectionObject = this.selectionManager.getSelectionObject();
if (selectionObject) {
try {
this._clonedSelectionObject = await this._cloneObject(
selectionObject
);
this._clonedSelectionObject = await this._cloneObject(selectionObject);
console.log("套索抠图:选区对象已克隆保存");
} catch (error) {
console.error("套索抠图:克隆选区对象失败:", error);
@@ -103,14 +94,7 @@ export class LassoCutoutCommand extends CompositeCommand {
const sourceObjects = this._getLayerObjects(activeLayer);
this.originalCanvasObjects = sourceObjects; // 保存真实对象引用
this.originalFabricObjects = sourceObjects.map((obj) =>
obj.toObject([
"id",
"layerId",
"layerName",
"parentId",
"type",
"custom",
])
obj.toObject(["id", "layerId", "layerName", "parentId", "type", "custom"])
);
console.log(
@@ -353,10 +337,7 @@ export class LassoCutoutCommand extends CompositeCommand {
await command.undo();
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令撤销失败: ${command.constructor.name}`, error);
// 子命令撤销失败不中断整个撤销过程
}
}
@@ -433,16 +414,11 @@ export class LassoCutoutCommand extends CompositeCommand {
// 递归获取图层及其子图层的所有对象
const collectLayerObjects = (currentLayer) => {
// 处理图层的fabricObjects
if (
currentLayer.fabricObjects &&
Array.isArray(currentLayer.fabricObjects)
) {
if (currentLayer.fabricObjects && Array.isArray(currentLayer.fabricObjects)) {
currentLayer.fabricObjects.forEach((fabricRef) => {
if (fabricRef && fabricRef.id) {
// 从画布中查找真实的对象
const realObject = canvasObjects.find(
(obj) => obj.id === fabricRef.id
);
const realObject = canvasObjects.find((obj) => obj.id === fabricRef.id);
if (realObject && realObject.visible) {
objects.push(realObject);
}
@@ -452,9 +428,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 处理单个fabricObject背景图层等
if (currentLayer.fabricObject && currentLayer.fabricObject.id) {
const realObject = canvasObjects.find(
(obj) => obj.id === currentLayer.fabricObject.id
);
const realObject = canvasObjects.find((obj) => obj.id === currentLayer.fabricObject.id);
if (realObject && realObject.visible) {
objects.push(realObject);
}
@@ -485,11 +459,7 @@ export class LassoCutoutCommand extends CompositeCommand {
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
) {
async _performCutoutWithRasterized(sourceObjects, selectionObject, selectionBounds) {
try {
console.log("=== 开始使用createRasterizedImage执行抠图 ===");
console.log(`源对象数量: ${sourceObjects.length}`);
@@ -528,10 +498,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 如果createRasterizedImage失败回退到原始方法
console.log("⚠️ 回退到原始抠图方法...");
return await this._performCutout(
{ fabricObjects: sourceObjects },
selectionObject
);
return await this._performCutout({ fabricObjects: sourceObjects }, selectionObject);
}
}
@@ -562,9 +529,7 @@ export class LassoCutoutCommand extends CompositeCommand {
try {
// 收集源图层中的可见对象
const visibleObjects = sourceLayer.fabricObjects.filter(
(obj) => obj.visible
);
const visibleObjects = sourceLayer.fabricObjects.filter((obj) => obj.visible);
if (visibleObjects.length === 0) {
throw new Error("源图层没有可见对象");
@@ -617,10 +582,7 @@ export class LassoCutoutCommand extends CompositeCommand {
let highResolutionScale = 1;
if (this.highResolutionEnabled) {
const devicePixelRatio = window.devicePixelRatio || 1;
highResolutionScale = Math.max(
this.baseResolutionScale,
devicePixelRatio * 2
);
highResolutionScale = Math.max(this.baseResolutionScale, devicePixelRatio * 2);
}
// 创建用于导出的高分辨率canvas
@@ -631,12 +593,8 @@ export class LassoCutoutCommand extends CompositeCommand {
colorSpace: "srgb",
});
const actualWidth = Math.round(
renderBounds.width * highResolutionScale
);
const actualHeight = Math.round(
renderBounds.height * highResolutionScale
);
const actualWidth = Math.round(renderBounds.width * highResolutionScale);
const actualHeight = Math.round(renderBounds.height * highResolutionScale);
exportCanvas.width = actualWidth;
exportCanvas.height = actualHeight;
@@ -845,17 +803,14 @@ export class LassoCutoutCommand extends CompositeCommand {
}
});
} else if (objectType === "polygon") {
fabric.Polygon.fromObject(
this.serializedSelectionObject,
(polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
fabric.Polygon.fromObject(this.serializedSelectionObject, (polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
);
});
} else if (objectType === "rect") {
fabric.Rect.fromObject(this.serializedSelectionObject, (rect) => {
if (rect) {
@@ -866,30 +821,24 @@ export class LassoCutoutCommand extends CompositeCommand {
}
});
} else if (objectType === "ellipse" || objectType === "circle") {
fabric.Ellipse.fromObject(
this.serializedSelectionObject,
(ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
fabric.Ellipse.fromObject(this.serializedSelectionObject, (ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
);
});
} else {
// 通用对象反序列化
fabric.util.enlivenObjects(
[this.serializedSelectionObject],
(objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
fabric.util.enlivenObjects([this.serializedSelectionObject], (objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
);
});
}
});
} catch (error) {
@@ -913,11 +862,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 1. 恢复图层到原位置
if (this.originalLayerIndex !== -1) {
this.layerManager.layers.value.splice(
this.originalLayerIndex,
0,
this.originalLayer
);
this.layerManager.layers.value.splice(this.originalLayerIndex, 0, this.originalLayer);
} else {
// 如果没有保存索引,添加到末尾
this.layerManager.layers.value.push(this.originalLayer);
@@ -925,9 +870,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 2. 恢复fabric对象到画布
if (this.originalFabricObjects.length > 0) {
console.log(
`↩️ 恢复 ${this.originalFabricObjects.length} 个fabric对象`
);
console.log(`↩️ 恢复 ${this.originalFabricObjects.length} 个fabric对象`);
// 使用fabric.util.enlivenObjects批量反序列化对象
await new Promise((resolve, reject) => {
@@ -1076,12 +1019,7 @@ export class ClearSelectionCommand extends Command {
// 序列化选区对象和相关状态
this.originalSelectionState = {
// 选区对象的序列化数据
selectionObjectData: selectionObject.toObject([
"id",
"layerId",
"layerName",
"parentId",
]),
selectionObjectData: selectionObject.toObject(["id", "layerId", "layerName", "parentId"]),
// 选区路径数据
selectionPath: this.selectionManager.getSelectionPath(),
// 羽化值
@@ -1144,13 +1082,8 @@ export class ClearSelectionCommand extends Command {
return;
}
const {
selectionObjectData,
featherAmount,
selectionStyle,
position,
managerState,
} = this.originalSelectionState;
const { selectionObjectData, featherAmount, selectionStyle, position, managerState } =
this.originalSelectionState;
// 根据选区对象类型进行反序列化
const objectType = selectionObjectData.type;
@@ -1196,9 +1129,7 @@ export class ClearSelectionCommand extends Command {
// 恢复阴影(羽化效果)
if (selectionStyle.shadow) {
restoredObject.shadow = new fabric.Shadow(
selectionStyle.shadow
);
restoredObject.shadow = new fabric.Shadow(selectionStyle.shadow);
}
}
@@ -1283,9 +1214,7 @@ export class ClearSelectionCommand extends Command {
// 验证基本属性
const originalId = this.originalSelectionState.selectionId;
if (currentSelection.id !== originalId) {
console.warn(
`选区ID不匹配: 期望 ${originalId}, 实际 ${currentSelection.id}`
);
console.warn(`选区ID不匹配: 期望 ${originalId}, 实际 ${currentSelection.id}`);
return false;
}
@@ -1293,9 +1222,7 @@ export class ClearSelectionCommand extends Command {
const currentFeather = this.selectionManager.getFeatherAmount();
const originalFeather = this.originalSelectionState.featherAmount;
if (currentFeather !== originalFeather) {
console.warn(
`羽化值不匹配: 期望 ${originalFeather}, 实际 ${currentFeather}`
);
console.warn(`羽化值不匹配: 期望 ${originalFeather}, 实际 ${currentFeather}`);
return false;
}

View File

@@ -1,15 +1,7 @@
import {
createLayer,
findInChildLayers,
LayerType,
OperationType,
} from "../utils/layerHelper.js";
import { createLayer, findInChildLayers, LayerType, OperationType } from "../utils/layerHelper.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand, Command } from "./Command.js";
import {
CreateImageLayerCommand,
RemoveLayerCommand,
} from "./LayerCommands.js";
import { CreateImageLayerCommand, RemoveLayerCommand } from "./LayerCommands.js";
import { fabric } from "fabric-with-all";
import { generateId } from "../utils/helper.js";
import { ToolCommand } from "./ToolCommands.js";
@@ -69,9 +61,7 @@ export class LassoCutoutCommand extends CompositeCommand {
const selectionObject = this.selectionManager.getSelectionObject();
if (selectionObject) {
try {
this._clonedSelectionObject = await this._cloneObject(
selectionObject
);
this._clonedSelectionObject = await this._cloneObject(selectionObject);
console.log("套索抠图:选区对象已克隆保存");
} catch (error) {
console.error("套索抠图:克隆选区对象失败:", error);
@@ -107,14 +97,7 @@ export class LassoCutoutCommand extends CompositeCommand {
const sourceObjects = this._getLayerObjects(activeLayer);
this.originalCanvasObjects = sourceObjects; // 保存真实对象引用
this.originalFabricObjects = sourceObjects.map((obj) =>
obj.toObject([
"id",
"layerId",
"layerName",
"parentId",
"type",
"custom",
])
obj.toObject(["id", "layerId", "layerName", "parentId", "type", "custom"])
);
console.log(
@@ -385,10 +368,7 @@ export class LassoCutoutCommand extends CompositeCommand {
await command.undo();
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令撤销失败: ${command.constructor.name}`, error);
// 子命令撤销失败不中断整个撤销过程
}
}
@@ -465,16 +445,11 @@ export class LassoCutoutCommand extends CompositeCommand {
// 递归获取图层及其子图层的所有对象
const collectLayerObjects = (currentLayer) => {
// 处理图层的fabricObjects
if (
currentLayer.fabricObjects &&
Array.isArray(currentLayer.fabricObjects)
) {
if (currentLayer.fabricObjects && Array.isArray(currentLayer.fabricObjects)) {
currentLayer.fabricObjects.forEach((fabricRef) => {
if (fabricRef && fabricRef.id) {
// 从画布中查找真实的对象
const realObject = canvasObjects.find(
(obj) => obj.id === fabricRef.id
);
const realObject = canvasObjects.find((obj) => obj.id === fabricRef.id);
if (realObject && realObject.visible) {
objects.push(realObject);
}
@@ -484,9 +459,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 处理单个fabricObject背景图层等
if (currentLayer.fabricObject && currentLayer.fabricObject.id) {
const realObject = canvasObjects.find(
(obj) => obj.id === currentLayer.fabricObject.id
);
const realObject = canvasObjects.find((obj) => obj.id === currentLayer.fabricObject.id);
if (realObject && realObject.visible) {
objects.push(realObject);
}
@@ -517,11 +490,7 @@ export class LassoCutoutCommand extends CompositeCommand {
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
) {
async _performCutoutWithRasterized(sourceObjects, selectionObject, selectionBounds) {
try {
console.log("=== 开始使用createRasterizedImage执行抠图 ===");
console.log(`源对象数量: ${sourceObjects.length}`);
@@ -560,10 +529,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 如果createRasterizedImage失败回退到原始方法
console.log("⚠️ 回退到原始抠图方法...");
return await this._performCutout(
{ fabricObjects: sourceObjects },
selectionObject
);
return await this._performCutout({ fabricObjects: sourceObjects }, selectionObject);
}
}
@@ -594,9 +560,7 @@ export class LassoCutoutCommand extends CompositeCommand {
try {
// 收集源图层中的可见对象
const visibleObjects = sourceLayer.fabricObjects.filter(
(obj) => obj.visible
);
const visibleObjects = sourceLayer.fabricObjects.filter((obj) => obj.visible);
if (visibleObjects.length === 0) {
throw new Error("源图层没有可见对象");
@@ -649,10 +613,7 @@ export class LassoCutoutCommand extends CompositeCommand {
let highResolutionScale = 1;
if (this.highResolutionEnabled) {
const devicePixelRatio = window.devicePixelRatio || 1;
highResolutionScale = Math.max(
this.baseResolutionScale,
devicePixelRatio * 2
);
highResolutionScale = Math.max(this.baseResolutionScale, devicePixelRatio * 2);
}
// 创建用于导出的高分辨率canvas
@@ -663,12 +624,8 @@ export class LassoCutoutCommand extends CompositeCommand {
colorSpace: "srgb",
});
const actualWidth = Math.round(
renderBounds.width * highResolutionScale
);
const actualHeight = Math.round(
renderBounds.height * highResolutionScale
);
const actualWidth = Math.round(renderBounds.width * highResolutionScale);
const actualHeight = Math.round(renderBounds.height * highResolutionScale);
exportCanvas.width = actualWidth;
exportCanvas.height = actualHeight;
@@ -877,17 +834,14 @@ export class LassoCutoutCommand extends CompositeCommand {
}
});
} else if (objectType === "polygon") {
fabric.Polygon.fromObject(
this.serializedSelectionObject,
(polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
fabric.Polygon.fromObject(this.serializedSelectionObject, (polygon) => {
if (polygon) {
console.log("多边形选区对象反序列化成功");
resolve(polygon);
} else {
reject(new Error("多边形选区对象反序列化失败"));
}
);
});
} else if (objectType === "rect") {
fabric.Rect.fromObject(this.serializedSelectionObject, (rect) => {
if (rect) {
@@ -898,30 +852,24 @@ export class LassoCutoutCommand extends CompositeCommand {
}
});
} else if (objectType === "ellipse" || objectType === "circle") {
fabric.Ellipse.fromObject(
this.serializedSelectionObject,
(ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
fabric.Ellipse.fromObject(this.serializedSelectionObject, (ellipse) => {
if (ellipse) {
console.log("椭圆选区对象反序列化成功");
resolve(ellipse);
} else {
reject(new Error("椭圆选区对象反序列化失败"));
}
);
});
} else {
// 通用对象反序列化
fabric.util.enlivenObjects(
[this.serializedSelectionObject],
(objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
fabric.util.enlivenObjects([this.serializedSelectionObject], (objects) => {
if (objects && objects.length > 0) {
console.log("通用选区对象反序列化成功");
resolve(objects[0]);
} else {
reject(new Error("通用选区对象反序列化失败"));
}
);
});
}
});
} catch (error) {
@@ -945,11 +893,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 1. 恢复图层到原位置
if (this.originalLayerIndex !== -1) {
this.layerManager.layers.value.splice(
this.originalLayerIndex,
0,
this.originalLayer
);
this.layerManager.layers.value.splice(this.originalLayerIndex, 0, this.originalLayer);
} else {
// 如果没有保存索引,添加到末尾
this.layerManager.layers.value.push(this.originalLayer);
@@ -957,9 +901,7 @@ export class LassoCutoutCommand extends CompositeCommand {
// 2. 恢复fabric对象到画布
if (this.originalFabricObjects.length > 0) {
console.log(
`↩️ 恢复 ${this.originalFabricObjects.length} 个fabric对象`
);
console.log(`↩️ 恢复 ${this.originalFabricObjects.length} 个fabric对象`);
// 使用fabric.util.enlivenObjects批量反序列化对象
await new Promise((resolve, reject) => {
@@ -1108,12 +1050,7 @@ export class ClearSelectionCommand extends Command {
// 序列化选区对象和相关状态
this.originalSelectionState = {
// 选区对象的序列化数据
selectionObjectData: selectionObject.toObject([
"id",
"layerId",
"layerName",
"parentId",
]),
selectionObjectData: selectionObject.toObject(["id", "layerId", "layerName", "parentId"]),
// 选区路径数据
selectionPath: this.selectionManager.getSelectionPath(),
// 羽化值
@@ -1176,13 +1113,8 @@ export class ClearSelectionCommand extends Command {
return;
}
const {
selectionObjectData,
featherAmount,
selectionStyle,
position,
managerState,
} = this.originalSelectionState;
const { selectionObjectData, featherAmount, selectionStyle, position, managerState } =
this.originalSelectionState;
// 根据选区对象类型进行反序列化
const objectType = selectionObjectData.type;
@@ -1228,9 +1160,7 @@ export class ClearSelectionCommand extends Command {
// 恢复阴影(羽化效果)
if (selectionStyle.shadow) {
restoredObject.shadow = new fabric.Shadow(
selectionStyle.shadow
);
restoredObject.shadow = new fabric.Shadow(selectionStyle.shadow);
}
}
@@ -1315,9 +1245,7 @@ export class ClearSelectionCommand extends Command {
// 验证基本属性
const originalId = this.originalSelectionState.selectionId;
if (currentSelection.id !== originalId) {
console.warn(
`选区ID不匹配: 期望 ${originalId}, 实际 ${currentSelection.id}`
);
console.warn(`选区ID不匹配: 期望 ${originalId}, 实际 ${currentSelection.id}`);
return false;
}
@@ -1325,9 +1253,7 @@ export class ClearSelectionCommand extends Command {
const currentFeather = this.selectionManager.getFeatherAmount();
const originalFeather = this.originalSelectionState.featherAmount;
if (currentFeather !== originalFeather) {
console.warn(
`羽化值不匹配: 期望 ${originalFeather}, 实际 ${currentFeather}`
);
console.warn(`羽化值不匹配: 期望 ${originalFeather}, 实际 ${currentFeather}`);
return false;
}

View File

@@ -1,16 +1,13 @@
import { Command } from "./Command";
import {
createLayer,
findLayerRecursively,
LayerType,
OperationType,
} from "../utils/layerHelper";
import { createLayer, findLayerRecursively, LayerType, OperationType } from "../utils/layerHelper";
import { createStaticCanvas } from "../utils/canvasFactory";
import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
import { ToolCommand } from "./ToolCommands";
import {
findObjectById,
generateId,
getObjectZIndex,
insertObjectAtZIndex,
objectIsInCanvas,
optimizeCanvasRendering,
removeCanvasObjectByObject,
@@ -112,15 +109,11 @@ export class AddLayerCommand extends Command {
parentLayer.children = parentLayer.children || [];
parentLayer.children.splice(insertIndex, 0, newLayer);
console.log(
`新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})`
);
console.log(`新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})`);
} else {
// 当前激活图层是一级图层
// 在一级图层中,插入到激活图层之上
const activeLayerIndex = layers.findIndex(
(layer) => layer.id === currentActiveLayerId
);
const activeLayerIndex = layers.findIndex((layer) => layer.id === currentActiveLayerId);
insertIndex = Math.max(0, activeLayerIndex);
layers.splice(insertIndex, 0, newLayer);
@@ -273,9 +266,7 @@ export class PasteLayerCommand extends Command {
try {
objects.forEach((obj) => {
// 生成新的对象ID
const newObjId = `obj_${Date.now()}_${Math.floor(
Math.random() * 1000
)}`;
const newObjId = `obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
obj.id = newObjId;
obj.layerId = this.newLayerId;
obj.layerName = this.newLayer.name;
@@ -337,9 +328,7 @@ export class PasteLayerCommand extends Command {
if (!this.newLayer || !this.newLayerId) return;
// 从图层列表删除该图层
const index = this.layers.value.findIndex(
(layer) => layer.id === this.newLayerId
);
const index = this.layers.value.findIndex((layer) => layer.id === this.newLayerId);
if (index !== -1) {
this.layers.value.splice(index, 1);
}
@@ -405,9 +394,7 @@ export class RemoveLayerCommand extends Command {
this.activeLayerId = options.activeLayerId;
// 查找要删除的图层
this.layerIndex = this.layers.value.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = this.layers.value.findIndex((layer) => layer.id === this.layerId);
this.removedLayer = this.layers.value[this.layerIndex];
this.isActiveLayer = this.layerId === this.activeLayerId.value;
@@ -474,9 +461,7 @@ export class RemoveLayerCommand extends Command {
// 如果删除的是当前活动图层,需要更新活动图层
if (this.isActiveLayer) {
// 查找最近的非背景层作为新的活动图层
const newActiveLayer = this.layers.value.find(
(layer) => !layer.isBackground
);
const newActiveLayer = this.layers.value.find((layer) => !layer.isBackground);
if (newActiveLayer) {
this.activeLayerId.value = newActiveLayer.id;
} else {
@@ -554,30 +539,22 @@ export class MoveLayerCommand extends Command {
this.parentLayer = null; // 父图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
// 如果parent 有值 或者 layer 上有parentId 则视为子图层的置顶
if (parent?.id) {
// 查找子图层索引
this.layerIndex = parent?.children?.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = parent?.children?.findIndex((layer) => layer.id === this.layerId);
this.parentLayer = parent;
} else {
// 查找图层索引
this.layerIndex = this.layers.value.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = this.layers.value.findIndex((layer) => layer.id === this.layerId);
}
this.layer = layer;
this.originalIndex = this.layerIndex;
// 目标位置
this.targetIndex =
options.direction === "up" ? this.layerIndex - 1 : this.layerIndex + 1;
this.targetIndex = options.direction === "up" ? this.layerIndex - 1 : this.layerIndex + 1;
}
async execute() {
@@ -668,9 +645,7 @@ export class ToggleLayerVisibilityCommand extends Command {
// 更新画布上图层对象的可见性
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.visible = this.layer.visible;
});
@@ -711,10 +686,7 @@ export class ToggleChildLayerVisibilityCommand extends Command {
this.layerManager = options.layerManager;
// 查找父图层和子图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
this.parentLayer = parent;
this.childLayer = layer;
@@ -731,9 +703,7 @@ export class ToggleChildLayerVisibilityCommand extends Command {
// 更新画布上图层对象的可见性
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.visible = this.childLayer.visible;
@@ -792,9 +762,7 @@ export class RenameLayerCommand extends Command {
// 更新图层对象上的图层名称
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.layerName = this.newName;
@@ -811,9 +779,7 @@ export class RenameLayerCommand extends Command {
// 恢复图层对象上的图层名称
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.layerName = this.oldName;
@@ -848,10 +814,7 @@ export class LayerLockCommand extends Command {
// 查找图层(包括子图层)
// 查找图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
this.layer = layer;
this.parentLayer = parent || null; // 父图层
this.oldLocked = this.layer ? this.layer.locked : null;
@@ -869,11 +832,7 @@ export class LayerLockCommand extends Command {
layer.locked = locked;
// 如果是组图层,递归更新所有子图层
if (
layer.type === "group" &&
layer.children &&
Array.isArray(layer.children)
) {
if (layer.type === "group" && layer.children && Array.isArray(layer.children)) {
layer.children.forEach((child) => {
this._updateLayerLockState(child, locked);
});
@@ -895,11 +854,7 @@ export class LayerLockCommand extends Command {
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity();
console.log(
`${newLocked ? "锁定" : "解锁"}图层: ${this.layer.name} (ID: ${
this.layerId
})`
);
console.log(`${newLocked ? "锁定" : "解锁"}图层: ${this.layer.name} (ID: ${this.layerId})`);
return true;
}
@@ -911,9 +866,7 @@ export class LayerLockCommand extends Command {
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity();
console.log(
`恢复图层锁定状态: ${this.layer.name} (锁定: ${this.oldLocked})`
);
console.log(`恢复图层锁定状态: ${this.layer.name} (锁定: ${this.oldLocked})`);
}
}
@@ -961,9 +914,7 @@ export class SetLayerOpacityCommand extends Command {
// 更新画布上对象的不透明度
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.opacity = this.opacity;
@@ -982,9 +933,7 @@ export class SetLayerOpacityCommand extends Command {
// 更新画布上对象的不透明度
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.opacity = this.oldOpacity;
@@ -1038,9 +987,7 @@ export class SetLayerBlendModeCommand extends Command {
// 更新画布上对象的混合模式
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.globalCompositeOperation = this.blendMode;
@@ -1059,9 +1006,7 @@ export class SetLayerBlendModeCommand extends Command {
// 更新画布上对象的混合模式
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
const layerObjects = this.canvas.getObjects().filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.globalCompositeOperation = this.oldBlendMode;
@@ -1101,17 +1046,11 @@ export class MergeLayersCommand extends Command {
// 备份原图层
this.originalLayers = [...this.layers.value];
// 新图层ID
this.newLayerId = `merged_layer_${Date.now()}_${Math.floor(
Math.random() * 1000
)}`;
this.newLayerId = `merged_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
}
execute() {
if (
!this.layerIds ||
!Array.isArray(this.layerIds) ||
this.layerIds.length < 2
) {
if (!this.layerIds || !Array.isArray(this.layerIds) || this.layerIds.length < 2) {
console.error("合并图层至少需要两个图层");
return null;
}
@@ -1132,6 +1071,14 @@ export class MergeLayersCommand extends Command {
return null;
}
// 查找所有要组合的图层
const layersToGroup = this.layerIds
.map((id) => {
const tempLayer = this.layers.value.find((layer) => layer.id === id); // 需要给子图层加上parentId
return tempLayer ? { ...tempLayer, parentId: this.groupId } : false;
})
.filter(Boolean);
const topLayerIndex = Math.min(
...layersToGroup.map((layer) =>
this.layers.value.findIndex((fLayer) => fLayer.id === layer.id)
@@ -1164,9 +1111,7 @@ export class MergeLayersCommand extends Command {
});
// 移除原图层
this.layers.value = this.layers.value.filter(
(layer) => !this.layerIds.includes(layer.id)
);
this.layers.value = this.layers.value.filter((layer) => !this.layerIds.includes(layer.id));
// 插入新图层
this.layers.value.splice(topLayerIndex, 0, mergedLayer);
@@ -1227,18 +1172,13 @@ export class GroupLayersCommand extends Command {
this.originalLayers = [...this.layers.value];
// 新组ID
this.groupId =
generateId("group_layer_") ||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("group_layer_") || `group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.originalActiveLayerId = this.activeLayerId.value; // 备份原活动图层ID
}
async execute() {
if (
!this.layerIds ||
!Array.isArray(this.layerIds) ||
this.layerIds.length < 2
) {
if (!this.layerIds || !Array.isArray(this.layerIds) || this.layerIds.length < 2) {
console.error("组合图层至少需要两个图层");
return null;
}
@@ -1282,9 +1222,7 @@ export class GroupLayersCommand extends Command {
});
// 移除原图层
this.layers.value = this.layers.value.filter(
(layer) => !this.layerIds.includes(layer.id)
);
this.layers.value = this.layers.value.filter((layer) => !this.layerIds.includes(layer.id));
// 插入新组图层
this.layers.value.splice(topLayerIndex, 0, groupLayer);
@@ -1338,9 +1276,7 @@ export class UngroupLayersCommand extends Command {
async execute() {
// 查找组图层
const groupIndex = this.layers.value.findIndex(
(layer) => layer.id === this.groupId
);
const groupIndex = this.layers.value.findIndex((layer) => layer.id === this.groupId);
if (groupIndex === -1) {
console.error(`找不到组图层 ${this.groupId}`);
@@ -1361,11 +1297,11 @@ export class UngroupLayersCommand extends Command {
this.layers.value.splice(
groupIndex,
1,
...groupLayer.children?.map((child) => {
...(groupLayer?.children?.map((child) => {
// 为子图层添加parentId
delete child.parentId; // 删除parentId属性
return { ...child };
})
}) || {})
);
// 更新当前活动图层为第一个子图层
if (this.childLayerIds.length > 0 && this.activeLayerId) {
@@ -1411,18 +1347,14 @@ export class MergeLayerObjectsCommand extends Command {
// 备份原始对象,用于撤销
if (this.activeLayer && Array.isArray(this.activeLayer.fabricObjects)) {
this.originalObjects =
this.canvas
?.getObjects()
?.filter((fItem) => fItem.layerId === this.activeLayer.id) || [];
this.canvas?.getObjects()?.filter((fItem) => fItem.layerId === this.activeLayer.id) || [];
} else {
this.originalObjects = [];
}
// 新合并图像对象
this.mergedImage = null;
this.newImageId = `merged_image_${Date.now()}_${Math.floor(
Math.random() * 1000
)}`;
this.newImageId = `merged_image_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
}
async execute() {
@@ -1458,10 +1390,7 @@ export class MergeLayerObjectsCommand extends Command {
// 异步处理图像合并
try {
const mergedImage = await this._createMergedImageAsync(
objectsToMerge,
bounds
);
const mergedImage = await this._createMergedImageAsync(objectsToMerge, bounds);
this._setupMergedImage(mergedImage, bounds);
this._replaceObjects(mergedImage);
@@ -1611,9 +1540,7 @@ export class MergeLayerObjectsCommand extends Command {
});
tempCanvas.add(clonedObj);
console.log(
`添加对象 ${index + 1}/${objects.length}: ${obj.type || "unknown"}`
);
console.log(`添加对象 ${index + 1}/${objects.length}: ${obj.type || "unknown"}`);
} catch (error) {
console.error(`添加对象到临时画布时发生错误:`, error);
}
@@ -1672,10 +1599,7 @@ export class MergeLayerObjectsCommand extends Command {
});
// 如果有新的图像对象,也要移除
if (
this.fabricImage &&
this.canvas.getObjects().includes(this.fabricImage)
) {
if (this.fabricImage && this.canvas.getObjects().includes(this.fabricImage)) {
this.canvas.remove(this.fabricImage);
}
@@ -1691,9 +1615,7 @@ export class MergeLayerObjectsCommand extends Command {
// 更新缩略图
if (this.canvas.thumbnailManager) {
setTimeout(() => {
this.canvas.thumbnailManager.generateLayerThumbnail(
this.activeLayer.id
);
this.canvas.thumbnailManager.generateLayerThumbnail(this.activeLayer.id);
}, 100);
}
}
@@ -1737,9 +1659,7 @@ export class MergeLayerObjectsCommand extends Command {
// 更新缩略图
if (this.canvas.thumbnailManager) {
setTimeout(() => {
this.canvas.thumbnailManager.generateLayerThumbnail(
this.activeLayer.id
);
this.canvas.thumbnailManager.generateLayerThumbnail(this.activeLayer.id);
}, 100);
}
}
@@ -1827,8 +1747,7 @@ export class LayerObjectsToGroupCommand extends Command {
this.existingGroupId = null;
this.groupObjectId = null;
this.newGroupId =
generateId("group") ||
`group_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("group") || `group_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.wasGroupCreated = false;
// 保存原始位置信息
@@ -1889,10 +1808,7 @@ export class LayerObjectsToGroupCommand extends Command {
const newObjectsToAdd = [];
if (this.fabricImage) {
// 如果是重做,恢复新对象的原始位置
if (
!this.isFirstExecution &&
this.newObjectPositions.has(this.fabricImage.id)
) {
if (!this.isFirstExecution && this.newObjectPositions.has(this.fabricImage.id)) {
const savedPosition = this.newObjectPositions.get(this.fabricImage.id);
this.fabricImage.set({
left: savedPosition.left,
@@ -1905,9 +1821,7 @@ export class LayerObjectsToGroupCommand extends Command {
flipX: savedPosition.flipX,
flipY: savedPosition.flipY,
});
console.log(
`🔄 重做时恢复新对象位置: (${savedPosition.left}, ${savedPosition.top})`
);
console.log(`🔄 重做时恢复新对象位置: (${savedPosition.left}, ${savedPosition.top})`);
}
newObjectsToAdd.push(this.fabricImage);
@@ -1960,9 +1874,7 @@ export class LayerObjectsToGroupCommand extends Command {
if (!this.existingGroupId) {
const groupObjects =
this.activeLayer.fabricObjects.find(
(obj) => obj && obj.type === "group"
) || null;
this.activeLayer.fabricObjects.find((obj) => obj && obj.type === "group") || null;
this.existingGroupId = groupObjects?.id || null;
}
@@ -2062,9 +1974,7 @@ export class LayerObjectsToGroupCommand extends Command {
// 更新图层的对象列表
// this.activeLayer.fabricObjects = [groupObject];
this.activeLayer.fabricObjects = [
groupObject.toObject(["id", "layerId", "layerName"]),
];
this.activeLayer.fabricObjects = [groupObject.toObject(["id", "layerId", "layerName"])];
}
/**
@@ -2150,10 +2060,7 @@ export class LayerObjectsToGroupCommand extends Command {
restorePosition = { ...originalPosition };
} else {
// 计算对象在画布中的当前绝对位置
restorePosition = this._calculateObjectAbsolutePosition(
obj,
groupObject
);
restorePosition = this._calculateObjectAbsolutePosition(obj, groupObject);
}
objectsToRestore.push({
@@ -2208,9 +2115,7 @@ export class LayerObjectsToGroupCommand extends Command {
restoredObjects.push(obj);
console.log(
`✅ 恢复原始对象 ${obj.id || obj.type} 到位置 (${position.left}, ${
position.top
})`
`✅ 恢复原始对象 ${obj.id || obj.type} 到位置 (${position.left}, ${position.top})`
);
} catch (error) {
console.error(`恢复对象 ${obj.id || obj.type} 时发生错误:`, error);
@@ -2270,10 +2175,7 @@ export class LayerObjectsToGroupCommand extends Command {
const objectPoint = new fabric.Point(obj.left || 0, obj.top || 0);
// 应用组的变换矩阵计算绝对位置
const absolutePoint = fabric.util.transformPoint(
objectPoint,
groupTransform
);
const absolutePoint = fabric.util.transformPoint(objectPoint, groupTransform);
// 计算缩放比例
const totalScaleX = (group.scaleX || 1) * (obj.scaleX || 1);
@@ -2441,8 +2343,7 @@ export class CreateImageLayerCommand extends Command {
// 存储执行过程中的结果
this.newLayerId =
generateId("layer_image_") ||
`layer_image_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("layer_image_") || `layer_image_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.commands = [];
this.executedCommands = [];
}
@@ -2457,8 +2358,7 @@ export class CreateImageLayerCommand extends Command {
this.executedCommands = [];
// 生成图层名称
const fileName =
this.layerName || `图片 ${new Date().toLocaleTimeString()}`;
const fileName = this.layerName || `图片 ${new Date().toLocaleTimeString()}`;
this.fabricImage.set({
id: this.imageId,
@@ -2529,9 +2429,7 @@ export class CreateImageLayerCommand extends Command {
return true;
}
console.log(
`↩️ 开始撤销创建图片图层操作,共 ${this.executedCommands.length} 个子命令`
);
console.log(`↩️ 开始撤销创建图片图层操作,共 ${this.executedCommands.length} 个子命令`);
try {
// 逆序撤销已执行的命令
@@ -2547,10 +2445,7 @@ export class CreateImageLayerCommand extends Command {
}
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令撤销失败: ${command.constructor.name}`, error);
}
}
}
@@ -2583,10 +2478,7 @@ export class CreateImageLayerCommand extends Command {
}
console.log(`✅ 子命令回滚成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令回滚失败: ${command.constructor.name}`,
error
);
console.error(`❌ 子命令回滚失败: ${command.constructor.name}`, error);
}
}
}
@@ -2721,9 +2613,7 @@ export class ReorderChildLayersCommand extends Command {
async execute() {
// 查找父图层
const parentLayer = this.layers.value.find(
(layer) => layer.id === this.parentId
);
const parentLayer = this.layers.value.find((layer) => layer.id === this.parentId);
if (!parentLayer) return false;
// 获取子图层
@@ -2873,15 +2763,11 @@ export class CutLayerCommand extends Command {
// 生成新图层ID和名称
this.newLayerId =
generateId("layer_") ||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("layer_") || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
}
async execute() {
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
const sourceLayer = layer;
const parentLayer = parent;
@@ -2908,9 +2794,7 @@ export class CutLayerCommand extends Command {
blendMode: sourceLayer.blendMode,
fabricObjects: [],
children: sourceLayer.children ? [...sourceLayer.children] : [],
layerProperties: sourceLayer.layerProperties
? { ...sourceLayer.layerProperties }
: {},
layerProperties: sourceLayer.layerProperties ? { ...sourceLayer.layerProperties } : {},
metadata: sourceLayer.metadata ? { ...sourceLayer.metadata } : {},
parentId: parentLayer ? parentLayer.id : undefined, // 保持父图层关系
});
@@ -2920,11 +2804,8 @@ export class CutLayerCommand extends Command {
if (parentLayer) {
// 处理子图层在父图层的children数组中插入
const sourceChildIndex = parentLayer.children.findIndex(
(child) => child.id === this.layerId
);
insertIndex =
this.insertIndex !== null ? this.insertIndex : sourceChildIndex + 1;
const sourceChildIndex = parentLayer.children.findIndex((child) => child.id === this.layerId);
insertIndex = this.insertIndex !== null ? this.insertIndex : sourceChildIndex + 1;
// 插入到父图层的children数组中
parentLayer.children.splice(insertIndex, 0, this.newLayer);
@@ -2933,11 +2814,8 @@ export class CutLayerCommand extends Command {
this.childInsertIndex = insertIndex;
} else {
// 处理主图层:在主图层数组中插入
const sourceIndex = this.layers.value.findIndex(
(l) => l.id === this.layerId
);
insertIndex =
this.insertIndex !== null ? this.insertIndex : sourceIndex + 1;
const sourceIndex = this.layers.value.findIndex((l) => l.id === this.layerId);
insertIndex = this.insertIndex !== null ? this.insertIndex : sourceIndex + 1;
// 插入到主图层数组中
this.layers.value.splice(insertIndex, 0, this.newLayer);
@@ -2956,9 +2834,7 @@ export class CutLayerCommand extends Command {
// 重新渲染画布
await this.layerManager?.updateLayersObjectsInteractivity(false);
console.log(
`已复制图层:${newName} ${this.isChildLayer ? "(子图层)" : "(主图层)"}`
);
console.log(`已复制图层:${newName} ${this.isChildLayer ? "(子图层)" : "(主图层)"}`);
return this.newLayerId;
}
@@ -2975,9 +2851,7 @@ export class CutLayerCommand extends Command {
}
} else {
// 撤销主图层:从主图层数组中删除
const index = this.layers.value.findIndex(
(l) => l.id === this.newLayerId
);
const index = this.layers.value.findIndex((l) => l.id === this.newLayerId);
if (index !== -1) {
this.layers.value.splice(index, 1);
}
@@ -3007,9 +2881,7 @@ export class CutLayerCommand extends Command {
fabric.util.enlivenObjects(serializedObjects, (objects) => {
objects.forEach((obj) => {
// 生成新的对象ID
const newObjId = `obj_${Date.now()}_${Math.floor(
Math.random() * 1000
)}`;
const newObjId = `obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
obj.id = newObjId;
obj.layerId = this.newLayerId;
obj.layerName = this.newLayer.name;
@@ -3066,8 +2938,7 @@ export class CreateAdjustmentLayerCommand extends Command {
this.newLayer = null;
// 生成新图层ID
this.newLayerId =
generateId("adj_layer_") ||
`adj_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
generateId("adj_layer_") || `adj_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
}
execute() {
@@ -3089,9 +2960,7 @@ export class CreateAdjustmentLayerCommand extends Command {
// 计算插入位置
const insertIndex =
this.insertIndex !== null
? this.insertIndex
: this._getInsertIndexAboveActiveLayer();
this.insertIndex !== null ? this.insertIndex : this._getInsertIndexAboveActiveLayer();
// 插入新图层
this.layers.value.splice(insertIndex, 0, this.newLayer);
@@ -3155,9 +3024,7 @@ export class ApplyLayerStyleCommand extends Command {
return;
}
this.layer = layer;
this.oldStyles = this.layer
? { ...(this.layer.layerProperties?.styles || {}) }
: {};
this.oldStyles = this.layer ? { ...(this.layer.layerProperties?.styles || {}) } : {};
}
execute() {
@@ -3296,9 +3163,7 @@ export class LayerClippingMaskCommand extends Command {
if (this.maskLayerId) {
// 创建剪贴蒙版
const maskLayer = this.layers.value.find(
(l) => l.id === this.maskLayerId
);
const maskLayer = this.layers.value.find((l) => l.id === this.maskLayerId);
if (!maskLayer) {
console.error(`蒙版图层 ${this.maskLayerId} 不存在`);
return false;
@@ -3525,19 +3390,14 @@ export class ChangeFixedImageCommand extends Command {
this.retryCount = attempt;
if (attempt === this.maxRetries) {
throw new Error(
`图像加载失败,已重试${this.maxRetries}次: ${error.message}`
);
throw new Error(`图像加载失败,已重试${this.maxRetries}次: ${error.message}`);
}
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.warn(
`图像加载重试 ${attempt + 1}/${this.maxRetries}:`,
error.message
);
console.warn(`图像加载重试 ${attempt + 1}/${this.maxRetries}:`, error.message);
}
}
}
@@ -3545,9 +3405,7 @@ export class ChangeFixedImageCommand extends Command {
loadImage() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`)
);
reject(new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`));
}, this.timeoutMs);
fabric.Image.fromURL(
@@ -3585,15 +3443,9 @@ export class ChangeFixedImageCommand extends Command {
// 移除旧对象(如果存在)
if (this.previousObjectId) {
const { object: oldObject } = findObjectById(
this.canvas,
this.previousObjectId
);
const { object: oldObject } = findObjectById(this.canvas, this.previousObjectId);
if (oldObject) {
const removeSuccess = removeCanvasObjectByObject(
this.canvas,
oldObject
);
const removeSuccess = removeCanvasObjectByObject(this.canvas, oldObject);
if (!removeSuccess) {
console.warn("移除旧对象失败,但继续执行");
}
@@ -3650,97 +3502,75 @@ export class ChangeFixedImageCommand extends Command {
}
return new Promise((resolve, reject) => {
fabric.util.enlivenObjects(
[this.previousImage.objectData],
async (objects) => {
try {
const restoredImage = objects[0];
fabric.util.enlivenObjects([this.previousImage.objectData], async (objects) => {
try {
const restoredImage = objects[0];
await optimizeCanvasRendering(this.canvas, async () => {
// 移除当前对象
if (this.newObjectId) {
const { object: currentObject } = findObjectById(
this.canvas,
this.newObjectId
);
if (currentObject) {
removeCanvasObjectByObject(this.canvas, currentObject);
}
await optimizeCanvasRendering(this.canvas, async () => {
// 移除当前对象
if (this.newObjectId) {
const { object: currentObject } = findObjectById(this.canvas, this.newObjectId);
if (currentObject) {
removeCanvasObjectByObject(this.canvas, currentObject);
}
}
// 恢复之前的变换
if (this.previousTransform) {
restoredImage.set(this.previousTransform);
}
// 恢复之前的变换
if (this.previousTransform) {
restoredImage.set(this.previousTransform);
}
// 设置图层属性
restoredImage.set({
id: this.previousObjectId,
layerId: this.targetLayer.id,
layerName: this.targetLayer.name,
isBackground: this.targetLayer.isBackground,
isFixed: this.targetLayer.isFixed,
});
// 使用帮助函数在正确的z-index位置恢复对象
if (
this.previousZIndex !== undefined &&
this.previousZIndex >= 0
) {
const insertSuccess = insertObjectAtZIndex(
this.canvas,
restoredImage,
this.previousZIndex,
false
);
if (insertSuccess) {
console.log(`恢复图像到z-index位置: ${this.previousZIndex}`);
} else {
// 如果插入失败,回退到普通添加
this.canvas.add(restoredImage);
console.log("z-index恢复失败恢复图像添加到顶层");
}
} else {
// 如果没有保存的z-index添加到顶层
this.canvas.add(restoredImage);
console.log("恢复图像添加到顶层");
}
restoredImage.setCoords();
// 更新引用
this.targetLayer.fabricObject = restoredImage.toObject([
"id",
"layerId",
"type",
]);
this.layerManager.updateLayerObject(
this.targetLayer.id,
restoredImage
);
// 设置图层属性
restoredImage.set({
id: this.previousObjectId,
layerId: this.targetLayer.id,
layerName: this.targetLayer.name,
isBackground: this.targetLayer.isBackground,
isFixed: this.targetLayer.isFixed,
});
resolve();
} catch (error) {
reject(error);
}
// 使用帮助函数在正确的z-index位置恢复对象
if (this.previousZIndex !== undefined && this.previousZIndex >= 0) {
const insertSuccess = insertObjectAtZIndex(
this.canvas,
restoredImage,
this.previousZIndex,
false
);
if (insertSuccess) {
console.log(`恢复图像到z-index位置: ${this.previousZIndex}`);
} else {
// 如果插入失败,回退到普通添加
this.canvas.add(restoredImage);
console.log("z-index恢复失败恢复图像添加到顶层");
}
} else {
// 如果没有保存的z-index添加到顶层
this.canvas.add(restoredImage);
console.log("恢复图像添加到顶层");
}
restoredImage.setCoords();
// 更新引用
this.targetLayer.fabricObject = restoredImage.toObject(["id", "layerId", "type"]);
this.layerManager.updateLayerObject(this.targetLayer.id, restoredImage);
});
resolve();
} catch (error) {
reject(error);
}
);
});
});
}
async removeCurrentImage() {
await optimizeCanvasRendering(this.canvas, async () => {
if (this.newObjectId) {
const { object: currentObject } = findObjectById(
this.canvas,
this.newObjectId
);
const { object: currentObject } = findObjectById(this.canvas, this.newObjectId);
if (currentObject) {
const removeSuccess = removeCanvasObjectByObject(
this.canvas,
currentObject
);
const removeSuccess = removeCanvasObjectByObject(this.canvas, currentObject);
if (removeSuccess) {
this.targetLayer.fabricObject = null;
this.layerManager.updateLayerObject(this.targetLayer.id, null);
@@ -3797,10 +3627,7 @@ export class AddImageToLayerCommand extends Command {
this.validateInputs();
// 查找目标图层
const { layer } = findLayerRecursively(
this.layerManager?.layers?.value || [],
this.layerId
);
const { layer } = findLayerRecursively(this.layerManager?.layers?.value || [], this.layerId);
this.targetLayer = layer;
@@ -3859,10 +3686,7 @@ export class AddImageToLayerCommand extends Command {
this.canvas.remove(this.addedObject);
// 从图层管理器中移除
this.layerManager.removeObjectFromLayer(
this.addedObject.id,
this.layerId
);
this.layerManager.removeObjectFromLayer(this.addedObject.id, this.layerId);
this.isExecuted = false;
@@ -3925,19 +3749,14 @@ export class AddImageToLayerCommand extends Command {
this.retryCount = attempt;
if (attempt === this.maxRetries) {
throw new Error(
`图像加载失败,已重试${this.maxRetries}次: ${error.message}`
);
throw new Error(`图像加载失败,已重试${this.maxRetries}次: ${error.message}`);
}
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.warn(
`图像加载重试 ${attempt + 1}/${this.maxRetries}:`,
error.message
);
console.warn(`图像加载重试 ${attempt + 1}/${this.maxRetries}:`, error.message);
}
}
}
@@ -3945,9 +3764,7 @@ export class AddImageToLayerCommand extends Command {
loadImage() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`)
);
reject(new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`));
}, this.timeoutMs);
fabric.Image.fromURL(
@@ -4057,9 +3874,7 @@ export class RemoveChildLayerCommand extends Command {
this.parentLayer = parent;
this.childIndex =
this.parentLayer?.children?.findIndex(
(child) => child.id === this.layerId
) ?? -1;
this.parentLayer?.children?.findIndex((child) => child.id === this.layerId) ?? -1;
this.removedChild = this.parentLayer?.children?.[this.childIndex];
this.isActiveLayer = this.layerId === this.activeLayerId.value;
@@ -4074,10 +3889,7 @@ export class RemoveChildLayerCommand extends Command {
}
// 从画布中移除子图层中的所有对象
if (
this.removedChild.fabricObjects &&
this.removedChild.fabricObjects.length > 0
) {
if (this.removedChild.fabricObjects && this.removedChild.fabricObjects.length > 0) {
this.removedChild.fabricObjects.forEach((obj) => {
const { object } = findObjectById(this.canvas, obj.id);
if (object) {
@@ -4096,9 +3908,7 @@ export class RemoveChildLayerCommand extends Command {
this.activeLayerId.value = this.parentLayer.children[0].id;
} else {
this.activeLayerId.value =
this.layers.value.find(
(layer) => !layer.isBackground || !layer.isFixed
)?.[0]?.id || null;
this.layers.value.find((layer) => !layer.isBackground || !layer.isFixed)?.[0]?.id || null;
}
}
@@ -4233,10 +4043,7 @@ export class ChildLayerLockCommand extends Command {
this.layerManager = options.layerManager;
// 查找父图层和子图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
this.parentLayer = parent;
this.childLayer = layer;
this.oldLocked = this.childLayer ? this.childLayer.locked : null;

View File

@@ -21,9 +21,7 @@ export class LiquifyCommand extends Command {
constructor(options) {
super({
name: options.name || `液化操作: ${options.mode || "未知模式"}`,
description:
options.description ||
`使用${options.mode || "未知模式"}模式进行液化操作`,
description: options.description || `使用${options.mode || "未知模式"}模式进行液化操作`,
});
this.canvas = options.canvas;
@@ -96,9 +94,7 @@ export class LiquifyCommand extends Command {
// 对于图像对象我们需要保存src和元数据
const state = {
src: this.targetObject.getSrc ? this.targetObject.getSrc() : null,
element: this.targetObject._element
? this.targetObject._element.cloneNode(true)
: null,
element: this.targetObject._element ? this.targetObject._element.cloneNode(true) : null,
filters: this.targetObject.filters ? [...this.targetObject.filters] : [],
originalData: this.originalData,
targetLayerId: this.targetLayerId,
@@ -172,10 +168,7 @@ export class LiquifyCommand extends Command {
// 确保图层引用更新
const layer = this.layerManager.getLayerById(this.targetLayerId);
if (layer) {
if (
layer.type === "background" &&
layer.fabricObject === this.targetObject
) {
if (layer.type === "background" && layer.fabricObject === this.targetObject) {
layer.fabricObject = img;
} else if (layer.fabricObjects) {
const objIndex = layer.fabricObjects.indexOf(this.targetObject);
@@ -251,9 +244,7 @@ export class LiquifyDeformCommand extends LiquifyCommand {
super({
...options,
name: `液化变形: ${options.mode || "未知模式"}`,
description: `在(${options.x}, ${options.y})应用${
options.mode || "未知模式"
}变形`,
description: `在(${options.x}, ${options.y})应用${options.mode || "未知模式"}变形`,
});
this.x = options.x;
@@ -343,9 +334,7 @@ export class LiquifyDeformCommand extends LiquifyCommand {
layer.fabricObject = img;
} else if (layer.fabricObjects) {
const objIndex = layer.fabricObjects.findIndex(
(obj) =>
obj === this.savedState?.originalObject ||
obj === this.targetObject
(obj) => obj === this.savedState?.originalObject || obj === this.targetObject
);
if (objIndex !== -1) {
layer.fabricObjects[objIndex] = img;
@@ -374,8 +363,7 @@ export class CompositeLiquifyCommand extends Command {
constructor(options) {
super({
name: options.name || "液化操作组合",
description:
options.description || `包含${options.commands?.length || 0}个液化操作`,
description: options.description || `包含${options.commands?.length || 0}个液化操作`,
});
this.commands = options.commands || [];
@@ -551,10 +539,7 @@ export class LiquifyStateCommand extends Command {
}
// 通过引用管理器更新图像数据
await this.refManager.updateObjectImageData(
this.objectRefId,
this.finalImageData
);
await this.refManager.updateObjectImageData(this.objectRefId, this.finalImageData);
// 恢复液化管理器到最终状态
if (this.liquifyManager && this.finalLiquifyState) {
@@ -585,10 +570,7 @@ export class LiquifyStateCommand extends Command {
}
// 通过引用管理器恢复到初始快照
await this.refManager.restoreFromSnapshot(
this.objectRefId,
this.initialSnapshotId
);
await this.refManager.restoreFromSnapshot(this.objectRefId, this.initialSnapshotId);
// 恢复液化管理器到初始状态 - 关键修复
if (this.liquifyManager) {
@@ -604,18 +586,16 @@ export class LiquifyStateCommand extends Command {
// 强制设置原始图像数据为初始状态
if (this.liquifyManager.enhancedManager) {
this.liquifyManager.enhancedManager.originalImageData =
this.initialImageData;
this.liquifyManager.enhancedManager.currentImageData =
this._cloneImageData(this.initialImageData);
this.liquifyManager.enhancedManager.originalImageData = this.initialImageData;
this.liquifyManager.enhancedManager.currentImageData = this._cloneImageData(
this.initialImageData
);
// 重置激活渲染器的数据
if (this.liquifyManager.enhancedManager.activeRenderer) {
const renderer = this.liquifyManager.enhancedManager.activeRenderer;
renderer.originalImageData = this.initialImageData;
renderer.currentImageData = this._cloneImageData(
this.initialImageData
);
renderer.currentImageData = this._cloneImageData(this.initialImageData);
// 重置CPU渲染器的网格和变形历史
if (renderer.reset) {
@@ -741,8 +721,7 @@ export class LiquifyStateCommand extends Command {
timestamp: Date.now(),
properties: this.refManager._captureObjectState(fabricObject),
imageData: this.initialImageData,
eventListeners:
this.refManager.eventListeners.get(this.objectRefId) || {},
eventListeners: this.refManager.eventListeners.get(this.objectRefId) || {},
};
this.refManager.stateSnapshots.set(this.initialSnapshotId, snapshot);
@@ -772,8 +751,7 @@ export class LiquifyStateCommand extends Command {
timestamp: Date.now(),
properties: this.refManager._captureObjectState(fabricObject),
imageData: this.finalImageData,
eventListeners:
this.refManager.eventListeners.get(this.objectRefId) || {},
eventListeners: this.refManager.eventListeners.get(this.objectRefId) || {},
};
this.refManager.stateSnapshots.set(this.finalSnapshotId, snapshot);
@@ -804,8 +782,7 @@ export class LiquifyStateCommand extends Command {
// 捕获当前渲染器状态
activeRendererState: null,
// 捕获目标对象引用
targetObjectRef:
this.liquifyManager.targetObject ?? enhancedManager.targetObject,
targetObjectRef: this.liquifyManager.targetObject ?? enhancedManager.targetObject,
// 捕获初始化状态
initialized: this.liquifyManager.initialized || false,
};
@@ -832,13 +809,9 @@ export class LiquifyStateCommand extends Command {
params: { ...renderer.params },
currentMode: renderer.currentMode,
// 对于CPU渲染器还需要保存网格状态
meshState: renderer.mesh
? this._captureMeshState(renderer.mesh)
: null,
meshState: renderer.mesh ? this._captureMeshState(renderer.mesh) : null,
// 保存变形历史
deformHistory: renderer.deformHistory
? [...renderer.deformHistory]
: [],
deformHistory: renderer.deformHistory ? [...renderer.deformHistory] : [],
};
}
}
@@ -871,9 +844,7 @@ export class LiquifyStateCommand extends Command {
this.realtimeUpdater.targetObject = this.liquifyManager.targetObject;
// 如果有setTargetObject方法调用它
if (typeof this.realtimeUpdater.setTargetObject === "function") {
this.realtimeUpdater.setTargetObject(
this.liquifyManager.targetObject
);
this.realtimeUpdater.setTargetObject(this.liquifyManager.targetObject);
}
}
@@ -911,10 +882,8 @@ export class LiquifyStateCommand extends Command {
} else if (renderer.mesh && renderer._initMesh) {
// 如果没有保存的网格状态,重新初始化网格
renderer._initMesh(
rendererState.originalImageData?.width ||
renderer.originalImageData?.width,
rendererState.originalImageData?.height ||
renderer.originalImageData?.height
rendererState.originalImageData?.width || renderer.originalImageData?.width,
rendererState.originalImageData?.height || renderer.originalImageData?.height
);
}
@@ -978,13 +947,7 @@ export class LiquifyStateCommand extends Command {
* @private
*/
_restoreMeshState(mesh, meshState) {
if (
!mesh ||
!meshState ||
!Array.isArray(mesh) ||
!Array.isArray(meshState)
)
return;
if (!mesh || !meshState || !Array.isArray(mesh) || !Array.isArray(meshState)) return;
for (let i = 0; i < Math.min(mesh.length, meshState.length); i++) {
for (let j = 0; j < Math.min(mesh[i].length, meshState[i].length); j++) {
@@ -1043,8 +1006,7 @@ export class BatchLiquifyStateCommand extends Command {
constructor(options) {
super({
name: options.name || "批量液化操作",
description:
options.description || `批量液化${options.commands?.length || 0}个对象`,
description: options.description || `批量液化${options.commands?.length || 0}个对象`,
});
this.commands = options.commands || [];
@@ -1117,9 +1079,7 @@ export class BatchLiquifyStateCommand extends Command {
* @returns {Boolean} 是否有效
*/
isValid() {
return (
this.commands.length > 0 && this.commands.every((cmd) => cmd.isValid())
);
return this.commands.length > 0 && this.commands.every((cmd) => cmd.isValid());
}
/**

View File

@@ -34,11 +34,7 @@ export class SetActiveLayerCommand extends Command {
}
execute() {
const { layer } = findLayerRecursively(
this.layers.value,
this.layerId,
this.parentId
);
const { layer } = findLayerRecursively(this.layers.value, this.layerId, this.parentId);
this.newLayer = layer;
if (!this.newLayer) {
@@ -68,10 +64,7 @@ export class SetActiveLayerCommand extends Command {
this.canvas.discardActiveObject();
// 设置为新的图层下的对象为激活,但需要确保对象存在于画布上
if (
this.newLayer.fabricObjects &&
this.newLayer.fabricObjects.length > 0
) {
if (this.newLayer.fabricObjects && this.newLayer.fabricObjects.length > 0) {
const canvasObjects = this.canvas.getObjects();
const validObjects = this.newLayer.fabricObjects.filter(
(obj) => obj && canvasObjects.includes(obj)
@@ -153,9 +146,7 @@ export class AddObjectToLayerCommand extends Command {
this.layerManager = options.layerManager;
// 保存对象原始状态和ID
this.objectId =
this.fabricObject.id ||
`obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.objectId = this.fabricObject.id || `obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.fabricObject.set({
id: this.objectId,
layerId: options.layerId,
@@ -163,11 +154,7 @@ export class AddObjectToLayerCommand extends Command {
});
// 保存完整的对象状态,包括位置和变换信息
this.originalObjectState = this.fabricObject.toObject([
"id",
"layerId",
"layerName",
]);
this.originalObjectState = this.fabricObject.toObject(["id", "layerId", "layerName"]);
// // 新增:保存详细的位置和变换信息,用于重做时恢复
// this.originalPosition = {
@@ -279,8 +266,7 @@ export class AddObjectToLayerCommand extends Command {
// 将对象添加到图层的fabricObjects数组
layer.fabricObjects = layer.fabricObjects || [];
layer.fabricObjects.push(
this.fabricObject?.toObject?.(["id", "layerId", "layerName"]) ||
this.fabricObject
this.fabricObject?.toObject?.(["id", "layerId", "layerName"]) || this.fabricObject
);
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
@@ -327,16 +313,12 @@ export class AddObjectToLayerCommand extends Command {
// 从图层的fabricObjects数组中移除对象
if (layer.fabricObjects) {
layer.fabricObjects = layer.fabricObjects.filter(
(obj) => obj.id !== this.objectId
);
layer.fabricObjects = layer.fabricObjects.filter((obj) => obj.id !== this.objectId);
}
// 优化渲染 - 统一批处理 支持异步回调
optimizeCanvasRendering(this.canvas, async () => {
// 从画布移除对象
const object = this.canvas
.getObjects()
.find((obj) => obj.id === this.objectId);
const object = this.canvas.getObjects().find((obj) => obj.id === this.objectId);
if (object) {
// 先丢弃活动对象,避免控制点渲染错误
this.canvas.discardActiveObject();
@@ -410,9 +392,7 @@ export class RemoveObjectFromLayerCommand extends Command {
// 从图层的fabricObjects数组移除对象
if (layer.fabricObjects) {
layer.fabricObjects = layer.fabricObjects.filter(
(obj) => obj.id !== this.objectId
);
layer.fabricObjects = layer.fabricObjects.filter((obj) => obj.id !== this.objectId);
}
// 更新画布
@@ -639,19 +619,14 @@ export class ChangeFixedImageCommand extends Command {
this.retryCount = attempt;
if (attempt === this.maxRetries) {
throw new Error(
`图像加载失败,已重试${this.maxRetries}次: ${error.message}`
);
throw new Error(`图像加载失败,已重试${this.maxRetries}次: ${error.message}`);
}
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.warn(
`图像加载重试 ${attempt + 1}/${this.maxRetries}:`,
error.message
);
console.warn(`图像加载重试 ${attempt + 1}/${this.maxRetries}:`, error.message);
}
}
}
@@ -659,9 +634,7 @@ export class ChangeFixedImageCommand extends Command {
loadImage() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`)
);
reject(new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`));
}, this.timeoutMs);
fabric.Image.fromURL(
@@ -697,15 +670,9 @@ export class ChangeFixedImageCommand extends Command {
// 移除旧对象(如果存在)
if (this.previousObjectId) {
const { object: oldObject } = findObjectById(
this.canvas,
this.previousObjectId
);
const { object: oldObject } = findObjectById(this.canvas, this.previousObjectId);
if (oldObject) {
const removeSuccess = removeCanvasObjectByObject(
this.canvas,
oldObject
);
const removeSuccess = removeCanvasObjectByObject(this.canvas, oldObject);
if (!removeSuccess) {
console.warn("移除旧对象失败,但继续执行");
}
@@ -771,93 +738,75 @@ export class ChangeFixedImageCommand extends Command {
}
return new Promise((resolve, reject) => {
fabric.util.enlivenObjects(
[this.previousImage.objectData],
async (objects) => {
try {
const restoredImage = objects[0];
fabric.util.enlivenObjects([this.previousImage.objectData], async (objects) => {
try {
const restoredImage = objects[0];
await optimizeCanvasRendering(this.canvas, async () => {
// 移除当前对象
if (this.newObjectId) {
const { object: currentObject } = findObjectById(
this.canvas,
this.newObjectId
);
if (currentObject) {
removeCanvasObjectByObject(this.canvas, currentObject);
}
await optimizeCanvasRendering(this.canvas, async () => {
// 移除当前对象
if (this.newObjectId) {
const { object: currentObject } = findObjectById(this.canvas, this.newObjectId);
if (currentObject) {
removeCanvasObjectByObject(this.canvas, currentObject);
}
}
// 恢复之前的变换
if (this.previousTransform) {
restoredImage.set(this.previousTransform);
}
// 恢复之前的变换
if (this.previousTransform) {
restoredImage.set(this.previousTransform);
}
// 设置图层属性
restoredImage.set({
id: this.previousObjectId,
layerId: this.targetLayer.id,
layerName: this.targetLayer.name,
isBackground: this.targetLayer.isBackground,
isFixed: this.targetLayer.isFixed,
});
// 使用帮助函数在正确的z-index位置恢复对象
if (
this.previousZIndex !== undefined &&
this.previousZIndex >= 0
) {
const insertSuccess = insertObjectAtZIndex(
this.canvas,
restoredImage,
this.previousZIndex,
false
);
if (insertSuccess) {
console.log(`恢复图像到z-index位置: ${this.previousZIndex}`);
} else {
// 如果插入失败,回退到普通添加
this.canvas.add(restoredImage);
console.log("z-index恢复失败恢复图像添加到顶层");
}
} else {
// 如果没有保存的z-index添加到顶层
this.canvas.add(restoredImage);
console.log("恢复图像添加到顶层");
}
restoredImage.setCoords();
// 更新引用
this.targetLayer.fabricObject = restoredImage;
this.layerManager.updateLayerObject(
this.targetLayer.id,
restoredImage
);
// 设置图层属性
restoredImage.set({
id: this.previousObjectId,
layerId: this.targetLayer.id,
layerName: this.targetLayer.name,
isBackground: this.targetLayer.isBackground,
isFixed: this.targetLayer.isFixed,
});
resolve();
} catch (error) {
reject(error);
}
// 使用帮助函数在正确的z-index位置恢复对象
if (this.previousZIndex !== undefined && this.previousZIndex >= 0) {
const insertSuccess = insertObjectAtZIndex(
this.canvas,
restoredImage,
this.previousZIndex,
false
);
if (insertSuccess) {
console.log(`恢复图像到z-index位置: ${this.previousZIndex}`);
} else {
// 如果插入失败,回退到普通添加
this.canvas.add(restoredImage);
console.log("z-index恢复失败恢复图像添加到顶层");
}
} else {
// 如果没有保存的z-index添加到顶层
this.canvas.add(restoredImage);
console.log("恢复图像添加到顶层");
}
restoredImage.setCoords();
// 更新引用
this.targetLayer.fabricObject = restoredImage;
this.layerManager.updateLayerObject(this.targetLayer.id, restoredImage);
});
resolve();
} catch (error) {
reject(error);
}
);
});
});
}
async removeCurrentImage() {
await optimizeCanvasRendering(this.canvas, async () => {
if (this.newObjectId) {
const { object: currentObject } = findObjectById(
this.canvas,
this.newObjectId
);
const { object: currentObject } = findObjectById(this.canvas, this.newObjectId);
if (currentObject) {
const removeSuccess = removeCanvasObjectByObject(
this.canvas,
currentObject
);
const removeSuccess = removeCanvasObjectByObject(this.canvas, currentObject);
if (removeSuccess) {
this.targetLayer.fabricObject = null;
this.layerManager.updateLayerObject(this.targetLayer.id, null);
@@ -976,10 +925,7 @@ export class AddImageToLayerCommand extends Command {
this.canvas.remove(this.addedObject);
// 从图层管理器中移除
this.layerManager.removeObjectFromLayer(
this.addedObject.id,
this.layerId
);
this.layerManager.removeObjectFromLayer(this.addedObject.id, this.layerId);
this.isExecuted = false;
@@ -1042,19 +988,14 @@ export class AddImageToLayerCommand extends Command {
this.retryCount = attempt;
if (attempt === this.maxRetries) {
throw new Error(
`图像加载失败,已重试${this.maxRetries}次: ${error.message}`
);
throw new Error(`图像加载失败,已重试${this.maxRetries}次: ${error.message}`);
}
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.warn(
`图像加载重试 ${attempt + 1}/${this.maxRetries}:`,
error.message
);
console.warn(`图像加载重试 ${attempt + 1}/${this.maxRetries}:`, error.message);
}
}
}
@@ -1062,9 +1003,7 @@ export class AddImageToLayerCommand extends Command {
loadImage() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`)
);
reject(new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`));
}, this.timeoutMs);
fabric.Image.fromURL(
@@ -1318,23 +1257,16 @@ export class MoveLayerToTopCommand extends Command {
this.parentLayer = null; // 父图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
// 如果parent 有值 或者 layer 上有parentId 则视为子图层的置顶
if (parent?.id) {
// 查找子图层索引
this.layerIndex = parent?.children?.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = parent?.children?.findIndex((layer) => layer.id === this.layerId);
this.parentLayer = parent;
} else {
// 查找图层索引
this.layerIndex = this.layers.value.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = this.layers.value.findIndex((layer) => layer.id === this.layerId);
}
this.layer = layer;
@@ -1416,23 +1348,16 @@ export class MoveLayerToBottomCommand extends Command {
this.parentLayer = null; // 父图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
// 如果parent 有值 或者 layer 上有parentId 则视为子图层的置顶
if (parent?.id) {
// 查找子图层索引
this.layerIndex = parent?.children?.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = parent?.children?.findIndex((layer) => layer.id === this.layerId);
this.parentLayer = parent;
} else {
// 查找图层索引
this.layerIndex = this.layers.value.findIndex(
(layer) => layer.id === this.layerId
);
this.layerIndex = this.layers.value.findIndex((layer) => layer.id === this.layerId);
}
this.layer = layer;
@@ -1473,9 +1398,7 @@ export class MoveLayerToBottomCommand extends Command {
async undo() {
if (this.originalIndex !== -1 && this.layer) {
// 获取当前位置
const currentIndex = this.layers.value.findIndex(
(layer) => layer.id === this.layerId
);
const currentIndex = this.layers.value.findIndex((layer) => layer.id === this.layerId);
if (currentIndex !== -1) {
// 移除当前位置
@@ -1526,16 +1449,11 @@ export class MoveLayerToBottomCommand extends Command {
if (layer.isBackground && layer.fabricObject) {
// 背景图层
const originalObj = canvasObjects.find(
(o) => o.id === layer.fabricObject.id
);
const originalObj = canvasObjects.find((o) => o.id === layer.fabricObject.id);
if (originalObj) {
this.canvas.add(originalObj);
}
} else if (
Array.isArray(layer.fabricObjects) &&
layer.fabricObjects.length > 0
) {
} else if (Array.isArray(layer.fabricObjects) && layer.fabricObjects.length > 0) {
// 普通图层和固定图层
layer.fabricObjects.forEach((obj) => {
const originalObj = canvasObjects.find((o) => o.id === obj.id);

View File

@@ -1,11 +1,6 @@
import { Command } from "./Command";
import { fabric } from "fabric-with-all";
import {
LayerType,
OperationType,
createLayer,
findLayerRecursively,
} from "../utils/layerHelper";
import { LayerType, OperationType, createLayer, findLayerRecursively } from "../utils/layerHelper";
import {
generateId,
optimizeCanvasRendering,
@@ -39,10 +34,7 @@ export class RasterizeLayerCommand extends Command {
this.layerManager = options.layerManager;
// 查找目标图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
this.layer = layer;
this.parentLayer = parent;
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
@@ -102,9 +94,7 @@ export class RasterizeLayerCommand extends Command {
console.log(`✅ 图层 ${this.layer.name} 栅格化完成`);
this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
this.rasterizedLayerId
);
this.canvas?.thumbnailManager?.generateLayerThumbnail?.(this.rasterizedLayerId);
return this.rasterizedLayerId;
} catch (error) {
console.error("栅格化图层失败:", error);
@@ -350,10 +340,7 @@ export class RasterizeLayerCommand extends Command {
*/
_replaceLayerInStructure() {
// 1.当前如果是子图层,则插入到子图层的位置
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
let insertIndex = 0;
// 说明是子图层
@@ -374,11 +361,7 @@ export class RasterizeLayerCommand extends Command {
if (insertIndex !== -1) {
if (parent) {
const pIndex = this.layers.value.findIndex((l) => l.id === parent.id);
this.layers.value[pIndex].children?.splice?.(
insertIndex,
1,
this.rasterizedLayer
);
this.layers.value[pIndex].children?.splice?.(insertIndex, 1, this.rasterizedLayer);
} else {
this.layers.value.splice(insertIndex, 1, this.rasterizedLayer);
}
@@ -396,14 +379,9 @@ export class RasterizeLayerCommand extends Command {
async _getMaskObject() {
// 如果图层有clippingMask获取对应的fabric对象
if (this.layer?.clippingMask) {
const { object: maskObject } = findObjectById(
this.canvas,
this.layer.clippingMask.id
);
const { object: maskObject } = findObjectById(this.canvas, this.layer.clippingMask.id);
if (maskObject) {
console.log(
`📎 找到遮罩对象: ${maskObject.id}, 类型: ${maskObject.type}`
);
console.log(`📎 找到遮罩对象: ${maskObject.id}, 类型: ${maskObject.type}`);
return maskObject;
}
return await restoreFabricObject(this.layer.clippingMask);
@@ -449,10 +427,7 @@ export class ExportLayerToImageCommand extends Command {
this.layerManager = options.layerManager;
// 查找目标图层
const { layer, parent } = findLayerRecursively(
this.layers.value,
this.layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, this.layerId);
this.layer = layer;
this.parentLayer = parent;
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;

View File

@@ -1,8 +1,4 @@
import {
findObjectById,
generateId,
optimizeCanvasRendering,
} from "../utils/helper.js";
import { findObjectById, generateId, optimizeCanvasRendering } from "../utils/helper.js";
import { imageModeHandler } from "../utils/imageHelper.js";
import { LayerType, OperationType } from "../utils/layerHelper.js";
import { Command, CompositeCommand } from "./Command.js";
@@ -58,9 +54,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
const normalLayers = layers.filter((layer) => !layer.isBackground && !layer.isFixed);
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
throw new Error("缺少必要的图层结构");
@@ -76,9 +70,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
}
: null;
this.originalFixedObjects = fixedLayer.fabricObject
? [fixedLayer.fabricObject]
: [];
this.originalFixedObjects = fixedLayer.fabricObject ? [fixedLayer.fabricObject] : [];
this.originalNormalObjects = normalLayer.fabricObjects
? [...normalLayer.fabricObjects]
@@ -118,11 +110,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
await this._setupClothingImage(clothingImg, fixedLayer);
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
await this._setupRedGreenImage(
redGreenImg,
normalLayer,
this.clothingImage
);
await this._setupRedGreenImage(redGreenImg, normalLayer, this.clothingImage);
// 4. 确保背景图层大小和衣服地图大小一致
const backgroundObject = await this._setupBackgroundLayer(
@@ -207,13 +195,9 @@ export class BatchInitializeRedGreenModeCommand extends Command {
async _createAndActivateEmptyLayer() {
// 创建新的空白图层
const newLayerName = "绘制图层";
const newLayerId = await this.layerManager.createLayer(
newLayerName,
LayerType.BITMAP,
{
undoable: false,
}
);
const newLayerId = await this.layerManager.createLayer(newLayerName, LayerType.BITMAP, {
undoable: false,
});
// 设置为活动图层
if (newLayerId) {
@@ -228,19 +212,14 @@ export class BatchInitializeRedGreenModeCommand extends Command {
await optimizeCanvasRendering(this.canvas, async () => {
// 1. 恢复画布背景
if (this.originalCanvasBackground !== null) {
this.canvas.setBackgroundColor(
this.originalCanvasBackground,
() => {}
);
this.canvas.setBackgroundColor(this.originalCanvasBackground, () => {});
}
// 2. 恢复图层对象
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
const normalLayers = layers.filter((layer) => !layer.isBackground && !layer.isFixed);
// 移除当前添加的对象
if (this.clothingImage) {
@@ -252,9 +231,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
// 移除新创建的空白图层
if (this.newEmptyLayerId) {
const emptyLayerIndex = layers.findIndex(
(layer) => layer.id === this.newEmptyLayerId
);
const emptyLayerIndex = layers.findIndex((layer) => layer.id === this.newEmptyLayerId);
if (emptyLayerIndex !== -1) {
layers.splice(emptyLayerIndex, 1);
}
@@ -270,9 +247,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
// 恢复固定图层
if (fixedLayer) {
fixedLayer.fabricObject =
this.originalFixedObjects.length > 0
? this.originalFixedObjects[0]
: null;
this.originalFixedObjects.length > 0 ? this.originalFixedObjects[0] : null;
if (fixedLayer.fabricObject) {
this.canvas.add(fixedLayer.fabricObject);
@@ -311,8 +286,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
// 4. 恢复工具状态
if (this.toolManager && this.originalToolState) {
this.toolManager.isRedGreenMode =
this.originalToolState.isRedGreenMode;
this.toolManager.isRedGreenMode = this.originalToolState.isRedGreenMode;
if (this.originalToolState.currentTool) {
this.toolManager.setTool(this.originalToolState.currentTool);
}
@@ -434,10 +408,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
// 清除固定图层原有内容
if (fixedLayer.fabricObject) {
const { object } = findObjectById(
this.canvas,
fixedLayer.fabricObject.id
);
const { object } = findObjectById(this.canvas, fixedLayer.fabricObject.id);
if (object) this.canvas.remove(object);
}

View File

@@ -110,9 +110,7 @@ export class AddToSelectionCommand extends Command {
}
// 添加到选区
const result = await this.selectionManager.addToSelection(
this.newSelection
);
const result = await this.selectionManager.addToSelection(this.newSelection);
return result;
}
@@ -155,9 +153,7 @@ export class RemoveFromSelectionCommand extends Command {
}
// 从选区中移除
const result = await this.selectionManager.removeFromSelection(
this.removeSelection
);
const result = await this.selectionManager.removeFromSelection(this.removeSelection);
return result;
}
@@ -203,9 +199,7 @@ export class FeatherSelectionCommand extends Command {
}
// 应用羽化
const result = await this.selectionManager.featherSelection(
this.featherAmount
);
const result = await this.selectionManager.featherSelection(this.featherAmount);
return result;
}
@@ -347,8 +341,7 @@ export class CopySelectionToNewLayerCommand extends CompositeCommand {
}
// 确定源图层
const sourceId =
this.sourceLayerId || this.layerManager.getActiveLayerId();
const sourceId = this.sourceLayerId || this.layerManager.getActiveLayerId();
const sourceLayer = this.layerManager.getLayerById(sourceId);
if (!sourceLayer || !sourceLayer.fabricObjects) {
console.error("无法复制选区:源图层无效或为空");
@@ -356,10 +349,7 @@ export class CopySelectionToNewLayerCommand extends CompositeCommand {
}
// 创建新图层
this.newLayerId = await this.layerManager.createLayer(
this.newLayerName,
LayerType.EMPTY
);
this.newLayerId = await this.layerManager.createLayer(this.newLayerName, LayerType.EMPTY);
// 获取选区内的对象
const objectsToCopy = sourceLayer.fabricObjects.filter((obj) => {

View File

@@ -401,9 +401,7 @@ export class CreateTextCommand extends Command {
// 更新图层的对象列表
if (layer) {
layer.fabricObjects = layer.fabricObjects || [];
layer.fabricObjects.push(
this.textObject.toObject(["id", "layerId", "layerName"])
);
layer.fabricObjects.push(this.textObject.toObject(["id", "layerId", "layerName"]));
}
// 现在可以安全地设置为活动图层
@@ -416,9 +414,7 @@ export class CreateTextCommand extends Command {
this.layerManager?.toolManager?.setTool?.(OperationType.SELECT);
});
console.log(
`✅ 文本对象已创建: "${finalOptions.text}",位置: (${this.x}, ${this.y})`
);
console.log(`✅ 文本对象已创建: "${finalOptions.text}",位置: (${this.x}, ${this.y})`);
return this.textObject;
} catch (error) {
console.error("创建文本对象失败:", error);
@@ -467,15 +463,11 @@ export class CreateTextCommand extends Command {
parentLayer.children = parentLayer.children || [];
parentLayer.children.splice(insertIndex, 0, newLayer);
console.log(
`新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})`
);
console.log(`新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})`);
} else {
// 当前激活图层是一级图层
// 在一级图层中,插入到激活图层之上
const activeLayerIndex = layers.findIndex(
(layer) => layer.id === currentActiveLayerId
);
const activeLayerIndex = layers.findIndex((layer) => layer.id === currentActiveLayerId);
insertIndex = Math.max(0, activeLayerIndex);
layers.splice(insertIndex, 0, newLayer);
@@ -551,9 +543,7 @@ export class CreateTextCommand extends Command {
// 恢复原活动图层
if (this.oldActiveLayerId && this.layerManager) {
// 检查原活动图层是否还存在
const originalLayer = this.layerManager.getLayerById(
this.oldActiveLayerId
);
const originalLayer = this.layerManager.getLayerById(this.oldActiveLayerId);
if (originalLayer) {
this.layerManager.setActiveLayer(this.oldActiveLayerId);
} else {
@@ -603,9 +593,7 @@ export class CreateTextCommand extends Command {
// 从子图层中移除
if (positionInfo.parent.children && positionInfo.index >= 0) {
positionInfo.parent.children.splice(positionInfo.index, 1);
console.log(
`已从子图层移除: ${this.layerId} (父图层: ${positionInfo.parent.name})`
);
console.log(`已从子图层移除: ${this.layerId} (父图层: ${positionInfo.parent.name})`);
}
} else {
// 从一级图层中移除

View File

@@ -0,0 +1,244 @@
import { findLayerRecursively } from "../utils/layerHelper";
import { restoreFabricObject } from "../utils/objectHelper";
import { Command } from "./Command";
/**
* 更新组图层遮罩位置命令
* 当组图层的对象移动时,同步更新遮罩的位置
*/
export class UpdateGroupMaskPositionCommand extends Command {
constructor(options) {
super({
name: "更新组图层遮罩位置",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.deltaX = options.deltaX || 0;
this.deltaY = options.deltaY || 0;
this.maskInitialLeft = options.maskInitialLeft || 0;
this.maskInitialTop = options.maskInitialTop || 0;
this.isExecuteRealtime = options.isExecuteRealtime || false;
this.activeSelection = this.canvas.getActiveObject() || {};
this.isFristExecute = true;
// 查找目标图层
this.layer = findLayerRecursively(this.layers.value, this.layerId)?.layer;
if (!this.layer || !this.layer.clippingMask) {
console.warn(`图层 ${this.layerId} 不存在或没有遮罩`);
return false;
}
// 保存原始遮罩位置(用于撤销)
// 保存原始位置
this.originalMaskPosition = {
left: this.maskInitialLeft || 0,
top: this.maskInitialTop || 0,
};
// 收集当前选择的所有对象位置
this.originalObjectsPostion = this.activeSelection.getObjects().map((obj) => {
return {
left: obj.left || 0,
top: obj.top || 0,
id: obj.id,
};
});
this.originalSelectionPosition = {
left: this.activeSelection.left || 0,
top: this.activeSelection.top || 0,
};
console.log(
`🛠️ 初始化更新组图层遮罩位置命令: ${this.name}, 图层ID: ${this.layerId}, 初始位置: (${this.originalMaskPosition.left}, ${this.originalMaskPosition.top})`
);
this.newMaskPosition = null;
}
async execute() {
try {
// 计算新位置
const newLeft = this.maskInitialLeft + this.deltaX;
const newTop = this.maskInitialTop + this.deltaY;
// 更新遮罩位置
this.layer.clippingMask.left = newLeft;
this.layer.clippingMask.top = newTop;
this.newMaskPosition = {
left: newLeft,
top: newTop,
};
// 更新所有使用此遮罩的子图层对象
await this._updateChildObjectsClipPath(this.layer, false, true);
this.isFristExecute = false;
console.log(`✅ 组图层遮罩位置已更新: (${newLeft}, ${newTop})`);
return true;
} catch (error) {
console.error("更新组图层遮罩位置失败:", error);
return false;
}
}
async undo() {
try {
if (!this.originalMaskPosition) {
console.warn("没有原始遮罩位置数据,无法撤销");
return false;
}
// 查找目标图层
const layer = this.layers.value.find((l) => l.id === this.layerId);
if (!layer || !layer.clippingMask) {
console.warn(`图层 ${this.layerId} 不存在或没有遮罩`);
return false;
}
console.log(
`↶ 撤销更新组图层遮罩位置: ${this.originalMaskPosition.left}, 图层ID: ${this.originalMaskPosition.top}`
);
// 恢复原始位置
layer.clippingMask.left = this.originalMaskPosition.left;
layer.clippingMask.top = this.originalMaskPosition.top;
// 更新所有使用此遮罩的子图层对象
await this._updateChildObjectsClipPath(layer, true);
// await this.layerManager.updateLayersObjectsInteractivity();
// this.canvas.renderAll();
console.log(
`↶ 组图层遮罩位置已恢复: (${this.originalMaskPosition.left}, ${this.originalMaskPosition.top})`
);
return true;
} catch (error) {
console.error("撤销组图层遮罩位置更新失败:", error);
return false;
}
}
/**
* 实时执行(不记录到历史记录)
* 用于拖拽过程中的实时更新
*/
executeRealtime() {
try {
// 查找目标图层
const layer = this.layers.value.find((l) => l.id === this.layerId);
if (!layer || !layer.clippingMask) {
return false;
}
// 计算新位置
const newLeft = this.maskInitialLeft + this.deltaX;
const newTop = this.maskInitialTop + this.deltaY;
// 更新遮罩位置
layer.clippingMask.left = newLeft;
layer.clippingMask.top = newTop;
// 更新所有使用此遮罩的子图层对象(不需要等待)
this._updateChildObjectsClipPath(layer);
return true;
} catch (error) {
console.error("实时更新组图层遮罩位置失败:", error);
return false;
}
}
/**
* 更新子图层对象的裁剪路径
* @param {Object} layer 组图层对象
* @private
*/
async _updateChildObjectsClipPath(layer, isUndo = false, isExecute = false) {
if (!layer.children || layer.children.length === 0) {
return;
}
try {
// 重新创建遮罩对象
const clippingMaskFabricObject = await restoreFabricObject(layer.clippingMask, this.canvas);
if (!clippingMaskFabricObject) {
console.warn("无法恢复遮罩对象");
return;
}
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
// 更新所有子图层对象的裁剪路径
layer.children.forEach((childLayer) => {
// 更新 fabricObjects 中的对象
childLayer.fabricObjects?.forEach((obj) => {
const fabricObject = this.canvas.getObjects().find((o) => o.id === obj.id);
if (fabricObject) {
fabricObject.clipPath = clippingMaskFabricObject;
fabricObject.dirty = true;
fabricObject.setCoords();
}
});
// 更新单个 fabricObject
if (childLayer.fabricObject) {
const fabricObject = this.canvas
.getObjects()
.find((o) => o.id === childLayer.fabricObject.id);
if (fabricObject) {
fabricObject.clipPath = clippingMaskFabricObject;
fabricObject.dirty = true;
fabricObject.setCoords();
}
}
});
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 (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();
}
// 触发画布重新渲染
this.canvas.renderAll();
} catch (error) {
console.error("更新子图层对象裁剪路径失败:", error);
}
}
getInfo() {
return {
name: this.name,
layerId: this.layerId,
deltaX: this.deltaX,
deltaY: this.deltaY,
originalPosition: this.originalMaskPosition,
newPosition: this.newMaskPosition,
};
}
}

View File

@@ -43,11 +43,7 @@
>
+
</button>
<button
class="control-btn remove"
@click="removeMemorizedSize"
v-if="canRemoveSize"
>
<button class="control-btn remove" @click="removeMemorizedSize" v-if="canRemoveSize">
-
</button>
</div>
@@ -57,16 +53,9 @@
<!-- 1.这里加上过渡动画 颜色选择器 - 仅在特定工具下显示 -->
<transition name="color-picker-fade" mode="out-in">
<div
v-if="showColorPicker"
class="color-picker-container"
key="color-picker"
>
<div v-if="showColorPicker" class="color-picker-container" key="color-picker">
<label for="color-picker" class="current-color-label">
<div
class="current-color"
:style="{ backgroundColor: brushColor }"
></div>
<div class="current-color" :style="{ backgroundColor: brushColor }"></div>
</label>
<input
type="color"
@@ -115,9 +104,7 @@
></div>
</div>
<div class="tooltip-content">
<div class="tooltip-text">
{{ Math.round(brushOpacity * 100) }}%
</div>
<div class="tooltip-text">{{ Math.round(brushOpacity * 100) }}%</div>
<div class="tooltip-controls">
<button
class="control-btn add"
@@ -470,7 +457,9 @@ watch(
background: rgba(255, 255, 255, 0.8);
border-radius: 5px;
padding: 15px 3px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
z-index: 8;
backdrop-filter: blur(2px);
color: #333;
@@ -478,7 +467,9 @@ watch(
-webkit-user-select: none;
// 添加高度过渡动画
transition: height 0.3s ease-out, min-height 0.3s ease-out;
transition:
height 0.3s ease-out,
min-height 0.3s ease-out;
// overflow: hidden;
transform: translate3d(0, -50%, 0); // 确保使用3D变换以提高性能
@@ -492,8 +483,7 @@ watch(
display: flex;
justify-content: center;
align-items: center;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
border-radius: 6px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 5px;
@@ -521,8 +511,7 @@ watch(
left: 0;
width: 100%;
height: 100%;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
}
.opacity-color {
@@ -617,13 +606,17 @@ watch(
height: 32px;
border-radius: 8px;
border: 2px solid #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
cursor: pointer;
&:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(0, 0, 0, 0.1);
}
}
@@ -683,7 +676,9 @@ watch(
// 淡入淡出动画
.brush-control-canel-fade-enter-active,
.brush-control-canel-fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.brush-control-canel-fade-enter-from,
.brush-control-canel-fade-leave-to {
@@ -694,7 +689,9 @@ watch(
// 颜色选择器过渡动画
.color-picker-fade-enter-active,
.color-picker-fade-leave-active {
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out;
}
.color-picker-fade-enter-from,
.color-picker-fade-leave-to {
@@ -705,7 +702,9 @@ watch(
// 透明度滑块过渡动画
.opacity-slider-fade-enter-active,
.opacity-slider-fade-leave-active {
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out;
}
.opacity-slider-fade-enter-from,
.opacity-slider-fade-leave-to {

View File

@@ -12,25 +12,16 @@
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
:class="['brush-type-item', { active: brushStore.state.type === brush.id }]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<div class="brush-preview" :style="getBrushPreviewStyle(brush)"></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<template v-for="(properties, category) in propertiesByCategory" :key="category">
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
@@ -43,11 +34,7 @@
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<div v-for="prop in properties" :key="prop.id" class="property-item">
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
@@ -61,13 +48,7 @@
<input
type="range"
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
"
@input="(e) => handlePropertyChange(prop.id, parseFloat(e.target.value))"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
@@ -93,25 +74,19 @@
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
<div class="color-preview" :style="{ backgroundColor: prop.value }"></div>
</div>
<div class="color-row">
<input
type="color"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@input="(e) => handlePropertyChange(prop.id, e.target.value)"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state
.recentColors"
v-for="(color, index) in brushStore.state.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@@ -130,9 +105,7 @@
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
@change="(e) => handlePropertyChange(prop.id, e.target.checked)"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
@@ -146,9 +119,7 @@
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@change="(e) => handlePropertyChange(prop.id, e.target.value)"
class="property-select"
>
<option
@@ -225,10 +196,7 @@
<!-- 上传的纹理缓存区域 -->
<!-- 自定义纹理上传按钮 -->
<div
class="texture-item upload-item"
@click="triggerTextureUpload"
>
<div class="texture-item upload-item" @click="triggerTextureUpload">
<div class="upload-icon">
<span>+</span>
</div>
@@ -275,9 +243,7 @@
<input
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@input="(e) => handlePropertyChange(prop.id, e.target.value)"
class="property-input"
/>
</div>
@@ -360,13 +326,7 @@
<input
type="checkbox"
:checked="brushStore.state.shadowEnabled"
@change="
(e) =>
handleShadowPropertyChange(
'shadowEnabled',
e.target.checked
)
"
@change="(e) => handleShadowPropertyChange('shadowEnabled', e.target.checked)"
id="shadow-enabled"
/>
<label for="shadow-enabled"></label>
@@ -390,13 +350,7 @@
<input
type="color"
:value="brushStore.state.shadowColor"
@input="
(e) =>
handleShadowPropertyChange(
'shadowColor',
e.target.value
)
"
@input="(e) => handleShadowPropertyChange('shadowColor', e.target.value)"
class="color-picker"
/>
</div>
@@ -408,20 +362,14 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影宽度") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowWidth }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowWidth }}px</span>
</div>
<div class="slider-container">
<input
type="range"
:value="brushStore.state.shadowWidth"
@input="
(e) =>
handleShadowPropertyChange(
'shadowWidth',
parseFloat(e.target.value)
)
(e) => handleShadowPropertyChange('shadowWidth', parseFloat(e.target.value))
"
:min="0"
:max="50"
@@ -435,8 +383,7 @@
:key="preset"
@click="handleShadowPropertyChange('shadowWidth', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
active: Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
}"
>
{{ preset }}
@@ -450,9 +397,7 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影X偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetX }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowOffsetX }}px</span>
</div>
<div class="slider-container">
<input
@@ -460,10 +405,7 @@
:value="brushStore.state.shadowOffsetX"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetX',
parseFloat(e.target.value)
)
handleShadowPropertyChange('shadowOffsetX', parseFloat(e.target.value))
"
:min="-50"
:max="50"
@@ -475,13 +417,9 @@
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetX', preset)
"
@click="handleShadowPropertyChange('shadowOffsetX', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetX - preset) <
0.1,
active: Math.abs(brushStore.state.shadowOffsetX - preset) < 0.1,
}"
>
{{ preset }}
@@ -495,9 +433,7 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影Y偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetY }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowOffsetY }}px</span>
</div>
<div class="slider-container">
<input
@@ -505,10 +441,7 @@
:value="brushStore.state.shadowOffsetY"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetY',
parseFloat(e.target.value)
)
handleShadowPropertyChange('shadowOffsetY', parseFloat(e.target.value))
"
:min="-50"
:max="50"
@@ -520,13 +453,9 @@
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetY', preset)
"
@click="handleShadowPropertyChange('shadowOffsetY', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetY - preset) <
0.1,
active: Math.abs(brushStore.state.shadowOffsetY - preset) < 0.1,
}"
>
{{ preset }}
@@ -544,14 +473,8 @@
class="shadow-preview-element"
:style="{
backgroundColor: brushStore.state.color,
width: `${Math.max(
20,
Math.min(60, brushStore.state.size)
)}px`,
height: `${Math.max(
20,
Math.min(60, brushStore.state.size)
)}px`,
width: `${Math.max(20, Math.min(60, brushStore.state.size))}px`,
height: `${Math.max(20, Math.min(60, brushStore.state.size))}px`,
boxShadow: brushStore.state.shadowEnabled
? `${brushStore.state.shadowOffsetX}px ${brushStore.state.shadowOffsetY}px ${brushStore.state.shadowWidth}px ${brushStore.state.shadowColor}`
: 'none',
@@ -567,11 +490,7 @@
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<button class="save-preset-btn" @click="saveCurrentAsPreset" title="保存当前设置为预设">
<i class="save-icon">+</i>
</button>
</div>
@@ -694,9 +613,7 @@ const filteredTextures = computed(() => {
if (selectedCategory.value === "全部") {
return textureLibrary;
}
return textureLibrary.filter(
(texture) => texture.category === selectedCategory.value
);
return textureLibrary.filter((texture) => texture.category === selectedCategory.value);
});
// 从材质库选择材质
@@ -909,8 +826,7 @@ function getBrushPreviewStyle(brush) {
case "spray":
return {
...baseStyle,
backgroundImage:
"radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundImage: "radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundSize: "4px 4px",
backgroundPosition: "center",
opacity: 0.8,
@@ -931,8 +847,7 @@ function getBrushPreviewStyle(brush) {
case "rainbow":
return {
...baseStyle,
background:
"linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
background: "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
height: "3px",
};
case "texture":
@@ -957,10 +872,7 @@ function applyPresetWithCommand(presetIndex) {
// 保存当前设置为预设
function saveCurrentAsPreset() {
// 简单实现,可以后续优化为弹窗输入名称
const name = prompt(
"请输入预设名称:",
`预设 ${BrushStore.state.presets.length + 1}`
);
const name = prompt("请输入预设名称:", `预设 ${BrushStore.state.presets.length + 1}`);
if (name) {
const presetIndex = BrushStore.saveCurrentAsPreset(name);
// 应用新创建的预设(可选)
@@ -1740,7 +1652,9 @@ const brushStore = BrushStore;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
animation: modalSlideUp 0.3s ease;
}
@@ -1930,12 +1844,7 @@ const brushStore = BrushStore;
content: "";
flex: 1;
height: 1px;
background: linear-gradient(
to right,
transparent,
rgba(0, 0, 0, 0.08),
transparent
);
background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.08), transparent);
}
.uploaded-textures-divider span {
@@ -2022,8 +1931,7 @@ const brushStore = BrushStore;
}
.shadow-preview-box {
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
border-radius: 8px;
padding: 30px;
display: flex;

View File

@@ -1,13 +1,5 @@
<script setup>
import {
inject,
ref,
provide,
onMounted,
computed,
watch,
onUnmounted,
} from "vue";
import { inject, ref, provide, onMounted, computed, watch, onUnmounted } from "vue";
import { OperationType } from "../utils/layerHelper";
import BrushPanel from "./BrushPanel.vue";
import { BrushStore } from "../store/BrushStore";
@@ -50,9 +42,7 @@ const lastColor = ref("#ffffff");
// return props.activeTool === OperationType.DRAW;
// });
function updateCanvasSize(
{ width, height } = { width: props.width, height: props.height }
) {
function updateCanvasSize({ width, height } = { width: props.width, height: props.height }) {
if (!layerManager) {
console.warn("LayerManager 未初始化,无法调整背景层尺寸");
return;
@@ -366,11 +356,7 @@ onMounted(() => {
<!-- 绘图工具设置 -->
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
<div
class="btn"
:class="{ active: showBrushPanel }"
@click="toggleBrushPanel"
>
<div class="btn" :class="{ active: showBrushPanel }" @click="toggleBrushPanel">
<!-- <span class="setting-label">笔刷:</span>/ -->
<div class="brush-selector">
<SvgIcon name="CBrushTop" size="22"></SvgIcon>
@@ -385,11 +371,7 @@ onMounted(() => {
<!-- <span class="brush-dropdown"></span> -->
</div>
<!-- 笔刷面板 -->
<div
v-if="showBrushPanel"
class="brush-panel-container"
ref="brushPanelRef"
>
<div v-if="showBrushPanel" class="brush-panel-container" ref="brushPanelRef">
<Teleport to="body">
<BrushPanel />
</Teleport>

View File

@@ -16,9 +16,7 @@ onMounted(() => {
platform.value = {
isMac: keyboardManager.platform === "mac",
isIOS: keyboardManager.platform === "ios",
isIPad:
keyboardManager.platform === "ios" &&
/iPad/.test(window.navigator.userAgent),
isIPad: keyboardManager.platform === "ios" && /iPad/.test(window.navigator.userAgent),
isTouchDevice: keyboardManager.isTouchDevice,
isWindows: keyboardManager.platform === "windows",
isAndroid: keyboardManager.platform === "android",
@@ -58,10 +56,10 @@ function convertShortcuts(managerShortcuts) {
increaseBrushSize: "增加笔触大小",
decreaseBrushSize: "减小笔触大小",
toggleTempTool: "临时切换工具",
newLayer: "新建图层",
groupLayers: "组合图层",
ungroupLayers: "取消组合",
mergeLayers: "合并图层",
// newLayer: "新建图层",
// groupLayers: "组合图层",
// ungroupLayers: "取消组合",
// mergeLayers: "合并图层",
};
// 工具ID到显示名称的映射
@@ -69,11 +67,11 @@ function convertShortcuts(managerShortcuts) {
select: "选择模式",
draw: "绘画模式",
eraser: "橡皮擦模式",
eyedropper: "吸色工具",
// eyedropper: "吸色工具",
pan: "移动画布",
lasso: "套索工具",
area_custom: "自由选区工具",
wave: "波浪工具",
// area_custom: "自由选区工具",
// wave: "波浪工具",
liquify: "液化工具",
};
@@ -82,11 +80,7 @@ function convertShortcuts(managerShortcuts) {
let actionDisplay = actionDisplayMap[shortcut.action] || shortcut.action;
// 特殊处理工具选择
if (
shortcut.action === "selectTool" &&
shortcut.param &&
toolDisplayMap[shortcut.param]
) {
if (shortcut.action === "selectTool" && shortcut.param && toolDisplayMap[shortcut.param]) {
actionDisplay = toolDisplayMap[shortcut.param];
}
@@ -144,12 +138,12 @@ function generateDefaultShortcuts() {
mac: "Delete 或 ⌫",
touch: "长按选中元素后点击删除",
},
{
action: "全选",
windows: "Ctrl+A",
mac: "⌘+A",
touch: "无",
},
// {
// action: "全选",
// windows: "Ctrl+A",
// mac: "⌘+A",
// touch: "无",
// },
{
action: "复制",
windows: "Ctrl+C",
@@ -198,12 +192,12 @@ function generateDefaultShortcuts() {
mac: "E",
touch: "点击橡皮擦工具",
},
{
action: "吸色工具",
windows: "I",
mac: "I",
touch: "点击吸色工具",
},
// {
// action: "吸色工具",
// windows: "I",
// mac: "I",
// touch: "点击吸色工具",
// },
{
action: "增加笔触大小",
windows: "]",
@@ -251,39 +245,24 @@ function getShortcutForCurrentPlatform(shortcut) {
// 按分类获取快捷键
function getShortcutsByCategory(category) {
const categoryMap = {
basic: [
"撤销",
"重做",
"全选",
"复制",
"粘贴",
"剪切",
"删除选中元素",
"上传图片",
],
// basic: ["撤销", "重做", "全选", "复制", "粘贴", "剪切", "删除选中元素", "上传图片"],
basic: ["撤销", "重做", "复制", "粘贴", "剪切", "删除选中元素", "上传图片"],
view: ["缩放画布", "移动画布"],
tools: [
"绘画模式",
"选择模式",
"橡皮擦模式",
"吸色工具",
// "吸色工具",
"套索工具",
"自由选区工具",
"波浪工具",
// "自由选区工具",
// "波浪工具",
"液化工具",
],
brush: [
"增加笔触大小",
"减小笔触大小",
"增加材质图片大小",
"减小材质图片大小",
],
layer: ["新建图层", "组合图层", "取消组合", "合并图层"],
brush: ["增加笔触大小", "减小笔触大小", "增加材质图片大小", "减小材质图片大小"],
// layer: ["新建图层", "组合图层", "取消组合", "合并图层"],
};
return shortcuts.value.filter((s) =>
categoryMap[category]?.includes(s.action)
);
return shortcuts.value.filter((s) => categoryMap[category]?.includes(s.action));
}
</script>
@@ -313,10 +292,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('basic')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('basic')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -334,10 +310,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('view')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('view')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -355,10 +328,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('tools')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('tools')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -376,10 +346,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('brush')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('brush')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -387,7 +354,7 @@ function getShortcutsByCategory(category) {
</table>
</div>
<div class="shortcuts-category">
<!-- <div class="shortcuts-category">
<h3>图层操作</h3>
<table class="shortcuts-table">
<thead>
@@ -397,16 +364,13 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('layer')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('layer')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
</div> -->
<div class="touch-tips" v-if="platform.isTouchDevice">
<h3>触控设备提示</h3>

View File

@@ -145,11 +145,7 @@ const handleItemMouseEnter = (item, index, event) => {
// 处理鼠标在菜单项内移动
const handleItemMouseMove = (item, index, event) => {
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
if (
item.children &&
item.children.length > 0 &&
hoveredItem.value !== index
) {
if (item.children && item.children.length > 0 && hoveredItem.value !== index) {
const element = event.target.closest(".context-menu-item");
showSubmenu(item, index, element);
}
@@ -261,10 +257,7 @@ onUnmounted(() => {
>
<template v-for="(item, index) in items" :key="index">
<!-- 分隔线 -->
<div
v-if="item.type === 'divider'"
class="context-menu-divider"
></div>
<div v-if="item.type === 'divider'" class="context-menu-divider"></div>
<!-- 菜单项 -->
<div
@@ -294,38 +287,24 @@ onUnmounted(() => {
<span class="context-menu-shortcut" v-if="item.shortcut">
{{ item.shortcut }}
</span>
<span
class="context-menu-arrow"
v-if="item.children && item.children.length > 0"
>
<span class="context-menu-arrow" v-if="item.children && item.children.length > 0">
<SvgIcon name="CRight" size="12" />
</span>
<!-- 子菜单 -->
<transition name="context-submenu">
<div
v-if="
item.children &&
item.children.length > 0 &&
hoveredItem === index
"
v-if="item.children && item.children.length > 0 && hoveredItem === index"
class="context-submenu"
:class="{
'submenu-left':
submenuPositions.get(index)?.direction === 'left',
'submenu-left': submenuPositions.get(index)?.direction === 'left',
}"
@mouseenter="handleSubmenuMouseEnter(index)"
@mouseleave="handleSubmenuMouseLeave"
>
<template
v-for="(subItem, subIndex) in item.children"
:key="subIndex"
>
<template v-for="(subItem, subIndex) in item.children" :key="subIndex">
<!-- 子菜单分隔线 -->
<div
v-if="subItem.type === 'divider'"
class="context-menu-divider"
></div>
<div v-if="subItem.type === 'divider'" class="context-menu-divider"></div>
<!-- 子菜单项 -->
<div
@@ -342,9 +321,7 @@ onUnmounted(() => {
:name="subItem.icon"
size="14"
:style="{
transform: subItem.inverIcon
? `rotate(90deg)`
: 'none',
transform: subItem.inverIcon ? `rotate(90deg)` : 'none',
}"
/>
</span>

View File

@@ -250,13 +250,7 @@ function handleTouchEnd(event) {
function handleUpdateChildLayers(newChildren) {
// 更新当前组图层的children数组
console.log(
"更新子图层顺序:",
"父图层ID:",
props.layer.id,
"新顺序:",
newChildren
);
console.log("更新子图层顺序:", "父图层ID:", props.layer.id, "新顺序:", newChildren);
emit("update-child-layers", props.layer.id, newChildren);
}
@@ -383,21 +377,13 @@ function findParentLayerId() {
>
<!-- 拖拽手柄 -->
<div class="layer-drag-handle" :title="$t('拖拽排序')">
<SvgIcon
v-if="!isHidenDragHandle"
:name="isChild ? 'CSort' : 'CSort'"
:size="32"
></SvgIcon>
<SvgIcon v-if="!isHidenDragHandle" :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
</div>
<!-- 图层头部 -->
<div class="layer-header">
<!-- 多选复选框 -->
<div
v-if="isMultiSelectMode && !isChild"
class="layer-checkbox"
@click.stop
>
<div v-if="isMultiSelectMode && !isChild" class="layer-checkbox" @click.stop>
<Checkbox :checked="isSelected" @change="handleCheckboxChange" />
</div>
@@ -453,12 +439,7 @@ function findParentLayerId() {
>
<SvgIcon name="CLock" :size="18"></SvgIcon>
</span>
<span
v-else
class="status-icon"
:title="$t('未锁定')"
@click.stop="handleToggleLock"
>
<span v-else class="status-icon" :title="$t('未锁定')" @click.stop="handleToggleLock">
<SvgIcon name="CUnLock" :size="18"></SvgIcon>
</span>

View File

@@ -96,8 +96,7 @@ const handleRootLayersSort = (event) => {
// 获取被拖拽的图层ID
const draggedLayerId =
item.getAttribute("data-layer-id") ||
props.sortableRootLayers[oldIndex]?.id;
item.getAttribute("data-layer-id") || props.sortableRootLayers[oldIndex]?.id;
if (!draggedLayerId) {
console.error("❌ 无法获取被拖拽的图层ID");
@@ -150,25 +149,14 @@ const handleRootLayersSort = (event) => {
if (props.isChild) {
// 子图层事件处理
// 确保排序只影响当前组图层的children而不是全局layers
emit(
"child-layers-sort",
event,
props.sortableRootLayers,
props.parentLayerId
);
emit("child-layers-sort", event, props.sortableRootLayers, props.parentLayerId);
} else {
emit("root-layers-sort", event);
}
};
// 验证跨层级移动的有效性
const validateCrossLevelMove = (
layerId,
fromType,
toType,
fromParentId,
toParentId
) => {
const validateCrossLevelMove = (layerId, fromType, toType, fromParentId, toParentId) => {
// 查找图层
const layer = findLayerInHierarchy(layerId, fromType, fromParentId);
@@ -250,9 +238,7 @@ const handleDragRemove = (event) => {
const canDeleteComputed = computed(() => {
// 如果是子图层,检查父图层是否可以删除
if (props.isChild) {
const parentLayer = props.layers.find(
(layer) => layer.id === props.parentLayerId
);
const parentLayer = props.layers.find((layer) => layer.id === props.parentLayerId);
return parentLayer?.children?.length > 1;
}
// 否则直接返回根图层的可删除状态
@@ -305,23 +291,14 @@ const canDeleteComputed = computed(() => {
:is-editing="editingLayerId === layer.id"
:editing-name="editingLayerName"
:can-delete="
canDeleteComputed &&
!layer.isBackground &&
!layer.isFixed &&
!layer.locked
canDeleteComputed && !layer.isBackground && !layer.isFixed && !layer.locked
"
:expanded-group-ids="expandedGroupIds"
@click="(...args) => forwardEvent('layer-click', ...args)"
@double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@@ -330,18 +307,10 @@ const canDeleteComputed = computed(() => {
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@update:editing-name="(...args) => forwardEvent('update:editing-name', ...args)"
@toggle-group-expanded="(...args) => forwardEvent('toggle-group-expanded', ...args)"
@toggle-child-visibility="(...args) => forwardEvent('toggle-child-visibility', ...args)"
@toggle-child-lock="(...args) => forwardEvent('toggle-child-lock', ...args)"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
@@ -370,16 +339,10 @@ const canDeleteComputed = computed(() => {
:parentLayerId="layer.id"
:group-name="groupName"
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
@layer-double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@layer-double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@@ -388,33 +351,17 @@ const canDeleteComputed = computed(() => {
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@root-layers-sort="
(...args) => forwardEvent('root-layers-sort', ...args)
"
@child-layers-sort="
(...args) => forwardEvent('child-layers-sort', ...args)
"
@cross-level-move="
(...args) => forwardEvent('cross-level-move', ...args)
"
@select-child-layer="
(...args) => forwardEvent('select-child-layer', ...args)
"
@start-child-layer-edit="
(...args) => forwardEvent('start-child-layer-edit', ...args)
"
@child-context-menu="
(...args) => forwardEvent('child-context-menu', ...args)
"
@update:editing-name="(...args) => forwardEvent('update:editing-name', ...args)"
@root-layers-sort="(...args) => forwardEvent('root-layers-sort', ...args)"
@child-layers-sort="(...args) => forwardEvent('child-layers-sort', ...args)"
@cross-level-move="(...args) => forwardEvent('cross-level-move', ...args)"
@select-child-layer="(...args) => forwardEvent('select-child-layer', ...args)"
@start-child-layer-edit="(...args) => forwardEvent('start-child-layer-edit', ...args)"
@child-context-menu="(...args) => forwardEvent('child-context-menu', ...args)"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@toggle-child-lock="(...args) => forwardEvent('toggle-child-lock', ...args)"
@finish-child-layer-edit="
(...args) => forwardEvent('finish-child-layer-edit', ...args)
"
@@ -424,9 +371,7 @@ const canDeleteComputed = computed(() => {
@child-layer-edit-keydown="
(...args) => forwardEvent('child-layer-edit-keydown', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@toggle-group-expanded="(...args) => forwardEvent('toggle-group-expanded', ...args)"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
@@ -517,7 +462,9 @@ const canDeleteComputed = computed(() => {
// 跨层级拖拽目标区域高亮
.sortable-layers {
position: relative;
transition: background-color 0.2s ease, border-color 0.2s ease;
transition:
background-color 0.2s ease,
border-color 0.2s ease;
border-radius: 4px;
min-height: 40px; // 确保空组也有足够的拖拽区域

View File

@@ -72,26 +72,21 @@ const contextMenuItems = ref([]);
// 计算属性:可排序的根级图层(排除背景层和固定层)
const sortableRootLayers = computed(() => {
if (!layers) return [];
return layers.value.filter(
(layer) => !layer.parentId && !layer.isFixed && !layer.isBackground
);
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground);
});
// 计算属性:不可排序的固定图层(背景层和固定层)
const fixedLayers = computed(() => {
if (!layers) return [];
return layers.value.filter((layer) => {
if (props.showFixedLayer)
return !layer.parentId && (layer.isFixed || layer.isBackground);
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground);
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
});
});
// 计算属性:获取当前选中的图层
const selectedLayers = computed(() => {
return sortableRootLayers.value.filter((layer) =>
selectedLayerIds.value.includes(layer.id)
);
return sortableRootLayers.value.filter((layer) => selectedLayerIds.value.includes(layer.id));
});
// 计算属性:获取当前是否激活子图层
@@ -224,9 +219,7 @@ function toggleLayerSelection(layer, event) {
}
const isShift = event.shiftKey;
const layerIndex = sortableRootLayers.value.findIndex(
(l) => l.id === layer.id
);
const layerIndex = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
console.log("isShift", isShift);
if (isShift && lastSelectedIndex.value !== -1) {
@@ -386,9 +379,7 @@ async function ungroupSelectedLayer() {
try {
const childLayerIds = await layerManager?.ungroupLayers(groupLayer.id);
console.log(
`✅ 成功解组图层组: ${groupLayer.name}, 子图层: ${childLayerIds}`
);
console.log(`✅ 成功解组图层组: ${groupLayer.name}, 子图层: ${childLayerIds}`);
// 清除选择状态
clearSelection();
@@ -406,9 +397,7 @@ function deleteSelectedLayers() {
}
// 检查是否包含不能删除的图层
const undeletableLayers = selectedLayers.filter(
(layer) => layer.isBackground || layer.isFixed
);
const undeletableLayers = selectedLayers.filter((layer) => layer.isBackground || layer.isFixed);
if (undeletableLayers.length > 0) {
console.warn("选择的图层中包含背景层或固定层,无法删除");
@@ -417,10 +406,7 @@ function deleteSelectedLayers() {
// 检查删除后是否还有足够的普通图层
const remainingNormalLayers = layers.value.filter(
(layer) =>
!layer.isBackground &&
!layer.isFixed &&
!selectedLayerIds.value.includes(layer.id)
(layer) => !layer.isBackground && !layer.isFixed && !selectedLayerIds.value.includes(layer.id)
).length;
if (remainingNormalLayers < 1) {
@@ -460,9 +446,7 @@ function startEditing(layer) {
// 下一帧聚焦输入框并选中文本
nextTick(() => {
const inputElement = document.querySelector(
`input[data-layer-id="${layer.id}"]`
);
const inputElement = document.querySelector(`input[data-layer-id="${layer.id}"]`);
if (inputElement) {
inputElement.focus();
inputElement.select();
@@ -494,41 +478,41 @@ function handleEditKeydown(event) {
}
}
// 获取图层缩略图URL
function getLayerThumbnail(layerId) {
if (props.thumbnailManager) {
return props.thumbnailManager.getLayerThumbnail(layerId);
}
return null;
}
// 获取图层缩略图URL - 弃用
// function getLayerThumbnail(layerId) {
// if (props.thumbnailManager) {
// return props.thumbnailManager.getLayerThumbnail(layerId);
// }
// return null;
// }
// 获取图层类型图标
function getLayerTypeIcon(layer) {
if (!layer) return "🖼️";
// function getLayerTypeIcon(layer) {
// if (!layer) return "🖼️";
if (isGroupLayer(layer)) {
return "📁";
}
// if (isGroupLayer(layer)) {
// return "📁";
// }
if (layer.fabricObject) {
switch (layer.fabricObject.type) {
case "image":
return "🖼️";
case "text":
return "📝";
case "rect":
return "▢";
case "circle":
return "⬤";
case "path":
return "✎";
default:
return "⬤";
}
}
// if (layer.fabricObject) {
// switch (layer.fabricObject.type) {
// case "image":
// return "🖼️";
// case "text":
// return "📝";
// case "rect":
// return "▢";
// case "circle":
// return "⬤";
// case "path":
// return "✎";
// default:
// return "⬤";
// }
// }
return "🖼️";
}
// return "🖼️";
// }
// 获取图层的子图层
function getChildLayers(parentId) {
@@ -557,22 +541,22 @@ const toggleGroupExpanded = (groupId) => {
expandedGroupIds.value = new Set(expandedGroupIds.value);
};
// 渲染单个图层项(递归组件)
function renderLayerItem(layer, index) {
if (!layer) return null;
// // 渲染单个图层项(递归组件)
// function renderLayerItem(layer, index) {
// if (!layer) return null;
const isGroup = isGroupLayerType(layer);
const children = isGroup ? getChildLayers(layer.id) : [];
// const isGroup = isGroupLayerType(layer);
// const children = isGroup ? getChildLayers(layer.id) : [];
return {
id: layer.id,
name: layer.name,
isGroup: isGroup,
children: children,
fabricObject: layer.fabricObject,
visible: layer.visible,
};
}
// return {
// id: layer.id,
// name: layer.name,
// isGroup: isGroup,
// children: children,
// fabricObject: layer.fabricObject,
// visible: layer.visible,
// };
// }
// 处理图层点击事件
function handleLayerClick(layer, event) {
@@ -580,12 +564,7 @@ function handleLayerClick(layer, event) {
event.stopPropagation();
// 如果按住修饰键,执行多选逻辑
if (
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
isMultiSelectMode.value
) {
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
toggleLayerSelection(layer, event);
} else {
// 普通点击:进入单选模式
@@ -595,11 +574,7 @@ function handleLayerClick(layer, event) {
// 如果不是多选模式,才可激活图层
// 1.如果是组,则设置组下的第一个子图层为活动图层
// 2.否则直接设置活动图层
if (
isGroupLayerType(layer) &&
layer.children &&
layer.children.length > 0
) {
if (isGroupLayerType(layer) && layer.children && layer.children.length > 0) {
// 如果是组图层,设置第一个子图层为活动图层
layerManager?.setAllActiveGroupLayerCanvasObject?.(layer);
setActiveLayer(layer.children[0].id, { parentId: layer.id });
@@ -609,9 +584,7 @@ function handleLayerClick(layer, event) {
layerManager?.updateLayersObjectsInteractivity();
}
}
lastSelectedIndex.value = sortableRootLayers.value.findIndex(
(l) => l.id === layer.id
);
lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
}
}
@@ -675,10 +648,7 @@ function buildContextMenuItems(layer) {
{
label: "栅格化图层",
icon: "CPicture",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canRasterizeLayer?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canRasterizeLayer?.(layer.id),
action: () => {
rasterizeLayer(layer.id);
hideContextMenu();
@@ -688,10 +658,7 @@ function buildContextMenuItems(layer) {
{
label: "导出图层",
icon: "CExport",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canRasterizeLayer?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canRasterizeLayer?.(layer.id),
action: () => {
exportLayerToImage(layer.id);
hideContextMenu();
@@ -755,10 +722,7 @@ function buildContextMenuItems(layer) {
label: "置顶",
icon: "CBottom",
inverIcon: true, // 倒置图标
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToTop?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canMoveToTop?.(layer.id),
action: () => {
moveLayerToTop(layer.id);
hideContextMenu();
@@ -767,10 +731,7 @@ function buildContextMenuItems(layer) {
{
label: "向上移动",
icon: "CUp",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToTop?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canMoveToTop?.(layer.id),
action: () => {
moveLayerUp(layer.id);
hideContextMenu();
@@ -780,9 +741,7 @@ function buildContextMenuItems(layer) {
label: "向下移动",
icon: "CDown",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToBottom?.(layer.id),
layer.isBackground || layer.isFixed || !layerManager?.canMoveToBottom?.(layer.id),
action: () => {
moveLayerDown(layer.id);
hideContextMenu();
@@ -792,9 +751,7 @@ function buildContextMenuItems(layer) {
label: "置底",
icon: "CBottom",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToBottom?.(layer.id),
layer.isBackground || layer.isFixed || !layerManager?.canMoveToBottom?.(layer.id),
action: () => {
moveLayerToBottom(layer.id);
hideContextMenu();
@@ -856,18 +813,13 @@ function canDeleteLayers() {
if (selectedLayers.length === 0) return false;
// 检查是否包含不能删除的图层
const undeletableLayers = selectedLayers.filter(
(layer) => layer.isBackground || layer.isFixed
);
const undeletableLayers = selectedLayers.filter((layer) => layer.isBackground || layer.isFixed);
if (undeletableLayers.length > 0) return false;
// 检查删除后是否还有足够的普通图层
const remainingNormalLayers = layers.value.filter(
(layer) =>
!layer.isBackground &&
!layer.isFixed &&
!selectedLayerIds.value.includes(layer.id)
(layer) => !layer.isBackground && !layer.isFixed && !selectedLayerIds.value.includes(layer.id)
).length;
return remainingNormalLayers >= 1;
@@ -904,9 +856,7 @@ function startChildLayerEdit(childLayer) {
childLayer.tempName = childLayer.name;
nextTick(() => {
const inputElement = document.querySelector(
`input[data-child-layer-id="${childLayer.id}"]`
);
const inputElement = document.querySelector(`input[data-child-layer-id="${childLayer.id}"]`);
if (inputElement) {
inputElement.focus();
inputElement.select();
@@ -943,8 +893,7 @@ function handleChildLayerEditKeydown(event, childLayer) {
// 选中子图层
function selectChildLayer(layerId, parentId) {
if (!isMultiSelectMode.value)
layerManager?.setActiveLayer(layerId, { parentId });
if (!isMultiSelectMode.value) layerManager?.setActiveLayer(layerId, { parentId });
}
// 子图层右键菜单处理
@@ -991,8 +940,7 @@ function buildChildLayerContextMenuItems(childLayer) {
{
label: childLayer.visible ? "隐藏图层" : "显示图层",
icon: childLayer.visible ? "CUnEye" : "CEye",
action: () =>
toggleChildLayerVisibility(childLayer.id, childLayer.parentId),
action: () => toggleChildLayerVisibility(childLayer.id, childLayer.parentId),
},
{ type: "divider" },
// 移出组
@@ -1045,9 +993,7 @@ async function handleRootLayersSort(event) {
try {
layerManager?.reorderLayers(oldIndex, newIndex, layerId);
console.log(
`✅ 图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`
);
console.log(`✅ 图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`);
} catch (error) {
console.error("❌ 图层排序命令执行失败:", error);
emit("reorder-layers", {
@@ -1085,12 +1031,7 @@ async function handleChildLayersSort(event, childrenLayers, parentId) {
const layerToMove = childrenLayers[oldIndex];
if (!layerToMove) {
console.error(
"❌ 找不到要移动的子图层oldIndex:",
oldIndex,
"childLayers:",
childrenLayers
);
console.error("❌ 找不到要移动的子图层oldIndex:", oldIndex, "childLayers:", childrenLayers);
return;
}
@@ -1105,9 +1046,7 @@ async function handleChildLayersSort(event, childrenLayers, parentId) {
try {
layerManager?.moveLayerToIndex({ parentId, oldIndex, newIndex, layerId });
console.log(
`✅ 子图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`
);
console.log(`✅ 子图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`);
} catch (error) {
console.error("❌ 子图层排序命令执行失败:", error);
emit("reorder-child-layers", {
@@ -1168,11 +1107,7 @@ async function mergeGroupLayer(groupId) {
const childLayerId = await layerManager.mergeGroupLayers(groupId);
if (childLayerId) {
const groupLayer = layers.value.find((l) => l.id === groupId);
console.log(
`✅ 成功合并组图层: ${
groupLayer?.name || groupId
}, 生成 ${childLayerId} 图层`
);
console.log(`✅ 成功合并组图层: ${groupLayer?.name || groupId}, 生成 ${childLayerId} 图层`);
} else {
console.warn("合并组图层失败");
}
@@ -1284,8 +1219,7 @@ async function handleCrossLevelMove(moveData) {
目标图层不是组图层: "只能将图层移动到组图层中",
};
const userMessage =
errorMessages[error.message] || `移动失败: ${error.message}`;
const userMessage = errorMessages[error.message] || `移动失败: ${error.message}`;
// 这里可以触发一个全局的错误提示组件
// 暂时使用console.warn实际项目中应该替换为适当的提示方式
@@ -1315,9 +1249,7 @@ async function executeDirectMove(moveData) {
} else if (fromContainerType === "child" && fromParentId) {
sourceParent = layers.value.find((layer) => layer.id === fromParentId);
if (sourceParent && sourceParent.children) {
draggedLayer = sourceParent.children.find(
(child) => child.id === layerId
);
draggedLayer = sourceParent.children.find((child) => child.id === layerId);
}
}
@@ -1385,9 +1317,7 @@ async function moveRootToGroup(draggedLayer, toParentId, newIndex) {
}
// 从顶级图层数组中移除
const rootIndex = layers.value.findIndex(
(layer) => layer.id === draggedLayer.id
);
const rootIndex = layers.value.findIndex((layer) => layer.id === draggedLayer.id);
if (rootIndex !== -1) {
layers.value.splice(rootIndex, 1);
}
@@ -1432,9 +1362,7 @@ async function moveGroupToRoot(draggedLayer, fromParentId, newIndex) {
} else {
// 回退方案:手动更新图层关系
// 从源父图层的children中移除
const childIndex = sourceParent.children.findIndex(
(child) => child.id === draggedLayer.id
);
const childIndex = sourceParent.children.findIndex((child) => child.id === draggedLayer.id);
if (childIndex !== -1) {
sourceParent.children.splice(childIndex, 1);
}
@@ -1461,12 +1389,7 @@ async function moveGroupToRoot(draggedLayer, fromParentId, newIndex) {
}
// 在不同组之间移动
async function moveGroupToGroup(
draggedLayer,
fromParentId,
toParentId,
newIndex
) {
async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex) {
console.log("🔄 在不同组间移动:", {
layerId: draggedLayer.id,
fromParentId,
@@ -1488,18 +1411,11 @@ async function moveGroupToGroup(
// 使用 layerManager 的方法在组间移动
if (layerManager?.moveLayerBetweenGroups) {
await layerManager.moveLayerBetweenGroups(
draggedLayer.id,
fromParentId,
toParentId,
newIndex
);
await layerManager.moveLayerBetweenGroups(draggedLayer.id, fromParentId, toParentId, newIndex);
} else {
// 回退方案:手动更新图层关系
// 从源父图层中移除
const childIndex = sourceParent.children.findIndex(
(child) => child.id === draggedLayer.id
);
const childIndex = sourceParent.children.findIndex((child) => child.id === draggedLayer.id);
if (childIndex !== -1) {
sourceParent.children.splice(childIndex, 1);
@@ -1585,19 +1501,11 @@ async function moveGroupToGroup(
>
<SvgIcon name="CPlusTop" size="16"></SvgIcon>
</div>
<div
class="add-layer-btn action-btn"
@click="addLayer"
:title="$t('添加图层')"
>
<div class="add-layer-btn action-btn" @click="addLayer" :title="$t('添加图层')">
<SvgIcon name="CPlus" size="16"></SvgIcon>
</div>
<div
class="select-all-btn action-btn"
@click="selectAllLayers"
:title="$t('全选图层')"
>
<div class="select-all-btn action-btn" @click="selectAllLayers" :title="$t('全选图层')">
<SvgIcon name="CCheckbox" size="16"></SvgIcon>
</div>
</div>
@@ -1668,15 +1576,11 @@ async function moveGroupToGroup(
:editing-name="editingLayerName"
:can-delete="false"
:isHidenDragHandle="true"
@toggle-visibility="
(...args) => forwardEvent('toggle-layer-visibility', ...args)
"
@toggle-visibility="(...args) => forwardEvent('toggle-layer-visibility', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@update:editing-name="(...args) => forwardEvent('update:editing-name', ...args)"
/>
</div>

View File

@@ -374,10 +374,7 @@ onMounted(() => {
});
// 创建状态管理器
stateManager.value = new LiquifyStateManager(
props.canvas,
realtimeUpdater.value
);
stateManager.value = new LiquifyStateManager(props.canvas, realtimeUpdater.value);
}
// 监听画布事件
@@ -446,12 +443,7 @@ function showPanel(event) {
return;
}
console.log(
"显示液化面板,目标对象:",
targetObj.type,
"图层ID:",
targetLayerIdValue
);
console.log("显示液化面板,目标对象:", targetObj.type, "图层ID:", targetLayerIdValue);
// 更新面板状态,但保持当前模式不变
// 只有在首次显示面板或面板已关闭时才重置模式
@@ -521,8 +513,8 @@ function showPanel(event) {
const status = props.liquifyManager.getStatus
? props.liquifyManager.getStatus()
: props.liquifyManager.enhancedManager
? props.liquifyManager.enhancedManager.getStatus()
: {};
? props.liquifyManager.enhancedManager.getStatus()
: {};
webglAvailable.value = status.isWebGLAvailable || false;
@@ -553,8 +545,8 @@ function showPanel(event) {
const status = props.liquifyManager.getStatus
? props.liquifyManager.getStatus()
: props.liquifyManager.enhancedManager
? props.liquifyManager.enhancedManager.getStatus()
: {};
? props.liquifyManager.enhancedManager.getStatus()
: {};
webglAvailable.value = status.isWebGLAvailable || false;
@@ -613,11 +605,7 @@ function setTargetObject(obj) {
// 确保对象有唯一ID
if (!obj.id && !obj.objectId && !obj.uid) {
obj.id =
"liquify_target_" +
Date.now() +
"_" +
Math.random().toString(36).substr(2, 9);
obj.id = "liquify_target_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
}
targetObject.value = obj;
@@ -685,9 +673,7 @@ function updateParam(paramName, value) {
`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`
] === "function"
) {
props.liquifyManager[
`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`
](value);
props.liquifyManager[`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`](value);
} else {
console.warn(`❌ 液化管理器不支持设置参数: ${paramName}`);
}
@@ -770,19 +756,9 @@ async function getCurrentImageData(targetObject) {
tempCtx.drawImage(element, 0, 0, tempCanvas.width, tempCanvas.height);
// 获取ImageData
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
"✅ 成功获取当前图像状态,尺寸:",
imageData.width,
"x",
imageData.height
);
console.log("✅ 成功获取当前图像状态,尺寸:", imageData.width, "x", imageData.height);
return imageData;
} catch (error) {
console.warn("获取当前图像数据失败:", error);
@@ -861,9 +837,7 @@ async function handleMouseDown(event) {
const imageCoords = _convertFabricCoordsToImageCoords(pointer.x, pointer.y);
if (imageCoords) {
props.liquifyManager.startLiquifyOperation(imageCoords.x, imageCoords.y);
console.log(
`开始液化操作,图像坐标: (${imageCoords.x}, ${imageCoords.y})`
);
console.log(`开始液化操作,图像坐标: (${imageCoords.x}, ${imageCoords.y})`);
}
}
@@ -959,27 +933,15 @@ async function handleMouseUp() {
// 尝试从实时更新器获取当前图像数据
if (realtimeUpdater.value) {
try {
const currentImageElement =
realtimeUpdater.value.getTargetObject()?._element;
const currentImageElement = realtimeUpdater.value.getTargetObject()?._element;
if (currentImageElement) {
// 从当前图像元素创建ImageData
const tempCanvas = document.createElement("canvas");
tempCanvas.width = initialImageData.value.width;
tempCanvas.height = initialImageData.value.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.drawImage(
currentImageElement,
0,
0,
tempCanvas.width,
tempCanvas.height
);
finalImageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
tempCtx.drawImage(currentImageElement, 0, 0, tempCanvas.width, tempCanvas.height);
finalImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
"✅ 从实时更新器获取最终图像数据成功,尺寸:",
@@ -1000,19 +962,8 @@ async function handleMouseUp() {
tempCanvas.width = initialImageData.value.width;
tempCanvas.height = initialImageData.value.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.drawImage(
currentTarget._element,
0,
0,
tempCanvas.width,
tempCanvas.height
);
finalImageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
tempCtx.drawImage(currentTarget._element, 0, 0, tempCanvas.width, tempCanvas.height);
finalImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
"✅ 从目标对象获取最终图像数据成功,尺寸:",
@@ -1097,14 +1048,7 @@ async function applyLiquifyAtPoint(x, y) {
return;
}
console.log(
"原始坐标:",
x,
y,
"转换后图像坐标:",
imageCoords.x,
imageCoords.y
);
console.log("原始坐标:", x, y, "转换后图像坐标:", imageCoords.x, imageCoords.y);
// 获取当前参数
const params = {
@@ -1169,8 +1113,7 @@ async function applyLiquifyAtPoint(x, y) {
targetObject.value = updatedObject;
// 如果对象有新的ID也要更新ID
if (updatedObject.id || updatedObject.objectId || updatedObject.uid) {
targetObjectId.value =
updatedObject.id || updatedObject.objectId || updatedObject.uid;
targetObjectId.value = updatedObject.id || updatedObject.objectId || updatedObject.uid;
}
}
}
@@ -1184,8 +1127,7 @@ async function applyLiquifyAtPoint(x, y) {
if (avgOperationTime.value === 0) {
avgOperationTime.value = operationTime;
} else {
avgOperationTime.value =
0.7 * avgOperationTime.value + 0.3 * operationTime;
avgOperationTime.value = 0.7 * avgOperationTime.value + 0.3 * operationTime;
}
// 将性能指标传递给状态管理器
@@ -1265,7 +1207,7 @@ async function _updateImageOnCanvas(imageData) {
}
} else {
// 拖拽结束后进行完整的对象替换
await _replaceTargetObjectWithNewImage(dataURL);
// await _replaceTargetObjectWithNewImage(dataURL);
}
} catch (error) {
console.error("更新画布图像时出错:", error);
@@ -1325,9 +1267,7 @@ async function _replaceTargetWithNewImage(dataURL) {
if (layer.type === "background" || layer.isBackground) {
layer.fabricObject = img;
} else if (layer.fabricObjects) {
const objIndex = layer.fabricObjects.findIndex(
(obj) => obj.id === img.id
);
const objIndex = layer.fabricObjects.findIndex((obj) => obj.id === img.id);
if (objIndex !== -1) {
layer.fabricObjects[objIndex] = img;
}
@@ -1364,10 +1304,7 @@ function _convertFabricCoordsToImageCoords(fabricX, fabricY) {
const matrix = fabric.util.invertTransform(transform);
// 应用逆变换,将画布坐标转换为对象本地坐标
const localPoint = fabric.util.transformPoint(
new fabric.Point(fabricX, fabricY),
matrix
);
const localPoint = fabric.util.transformPoint(new fabric.Point(fabricX, fabricY), matrix);
// 获取图像的原始尺寸(未缩放前)
const imageWidth = originalImageData.value.width;
@@ -1403,12 +1340,7 @@ function _convertFabricCoordsToImageCoords(fabricX, fabricY) {
);
// 检查坐标是否在图像范围内
if (
imageX < 0 ||
imageX >= imageWidth ||
imageY < 0 ||
imageY >= imageHeight
) {
if (imageX < 0 || imageX >= imageWidth || imageY < 0 || imageY >= imageHeight) {
console.warn(
`坐标超出图像范围: (${imageX}, ${imageY}), 图像尺寸: ${imageWidth}x${imageHeight}`
);
@@ -1607,7 +1539,9 @@ function stopPressTimer() {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {

View File

@@ -68,9 +68,7 @@ onBeforeUnmount(() => {
<div class="minimap-container">
<div class="minimap-header">
<span>画布小地图</span>
<button class="minimap-refresh" @click="forceRefresh" title="刷新小地图">
</button>
<button class="minimap-refresh" @click="forceRefresh" title="刷新小地图"></button>
</div>
<div class="minimap-content" ref="minimapContainerRef">
<!-- 不再需要直接提供canvas引用由MinimapManager内部创建 -->

View File

@@ -10,30 +10,21 @@
<div class="tool-types">
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO },
]"
:class="['tool-btn', { active: selectionType === OperationType.LASSO }]"
@click="setSelectionType(OperationType.LASSO)"
>
<svg-icon name="CFree" size="26" />
<span>{{ $t("手绘") }}</span>
</div>
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO_RECTANGLE },
]"
:class="['tool-btn', { active: selectionType === OperationType.LASSO_RECTANGLE }]"
@click="setSelectionType(OperationType.LASSO_RECTANGLE)"
>
<svg-icon name="CRectangle" size="32" />
<span>{{ $t("矩形") }}</span>
</div>
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO_ELLIPSE },
]"
:class="['tool-btn', { active: selectionType === OperationType.LASSO_ELLIPSE }]"
@click="setSelectionType(OperationType.LASSO_ELLIPSE)"
>
<svg-icon name="CEllipse" size="30" />
@@ -159,9 +150,7 @@
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ $t("选择填充颜色") }}</h3>
<button class="close-dialog-btn" @click="cancelColorPicker">
×
</button>
<button class="close-dialog-btn" @click="cancelColorPicker">×</button>
</div>
<div class="dialog-content">
<input type="color" v-model="fillColor" class="color-picker" />
@@ -249,6 +238,7 @@ onMounted(() => {
checkSelectionStatus();
// 设置选区状态变化的回调
// eslint-disable-next-line vue/no-mutating-props
props.selectionManager.onSelectionChanged = () => {
checkSelectionStatus();
};
@@ -321,8 +311,7 @@ function setSelectionType(type) {
*/
function checkSelectionStatus() {
hasSelection.value =
props.selectionManager &&
props.selectionManager.getSelectionObject() !== null;
props.selectionManager && props.selectionManager.getSelectionObject() !== null;
// 同步羽化值
if (hasSelection.value) {
@@ -836,7 +825,9 @@ function confirmColorPicker() {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {

View File

@@ -1,10 +1,6 @@
<!-- filepath: /Users/aaron/work/pc/air/canvasEdit/src/components/CanvasEditor/components/TextEditorPanel.vue -->
<template>
<div
v-if="visible"
class="text-editor-panel"
:class="{ 'is-active': visible }"
>
<div v-if="visible" class="text-editor-panel" :class="{ 'is-active': visible }">
<div class="text-editor-panel-header">
<div class="header-btn import-btn">编辑文本样式</div>
<div class="header-actions">
@@ -185,10 +181,7 @@
<div class="bg-header">字体色</div>
<div class="bg-options">
<div class="style-btn color-btn" @click="openColorPicker('text')">
<div
class="style-icon color-icon"
:style="{ backgroundColor: textColor }"
></div>
<div class="style-icon color-icon" :style="{ backgroundColor: textColor }"></div>
</div>
</div>
</div>
@@ -196,22 +189,15 @@
<div class="background-controls">
<div class="bg-header">背景色</div>
<div class="bg-options">
<div
class="style-btn color-btn"
@click="openColorPicker('background')"
>
<div class="style-btn color-btn" @click="openColorPicker('background')">
<div
class="style-icon color-icon"
:style="{
backgroundColor: hasTransparentBg
? 'transparent'
: backgroundColor,
backgroundColor: hasTransparentBg ? 'transparent' : backgroundColor,
backgroundImage: hasTransparentBg
? 'linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc)'
: 'none',
backgroundSize: hasTransparentBg
? '8px 8px, 8px 8px'
: 'auto',
backgroundSize: hasTransparentBg ? '8px 8px, 8px 8px' : 'auto',
backgroundPosition: hasTransparentBg ? '0 0, 4px 4px' : '0 0',
}"
></div>
@@ -232,18 +218,11 @@
<div v-if="showColorPicker" class="color-picker-modal">
<div class="color-picker-container">
<div class="color-picker-header">
<span>{{
colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色"
}}</span>
<span>{{ colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色" }}</span>
<div class="close-color-picker" @click="closeColorPicker">×</div>
</div>
<div class="color-picker-content">
<input
type="color"
v-model="currentColor"
@change="updateColor"
class="color-input"
/>
<input type="color" v-model="currentColor" @change="updateColor" class="color-input" />
<div class="color-presets">
<div
v-for="(color, index) in colorPresets"
@@ -253,9 +232,7 @@
@click="selectPresetColor(color)"
></div>
</div>
<div class="confirm-color-btn" @click="confirmColorSelection">
确定
</div>
<div class="confirm-color-btn" @click="confirmColorSelection">确定</div>
</div>
</div>
</div>
@@ -316,7 +293,7 @@ export default {
// 新增属性
const charSpacingPercent = ref(0);
const textSpacing = ref(0);
const baseline = ref(0);
// const baseline = ref(0);
const showColorPicker = ref(false);
const colorPickerMode = ref("text"); // 'text' 或 'background'
const currentColor = ref("#000000");
@@ -406,8 +383,7 @@ export default {
textColor.value = textObject.value.fill || "#000000";
backgroundColor.value = textObject.value.textBackgroundColor || "#ffffff";
hasTransparentBg.value = !textObject.value.textBackgroundColor;
opacity.value =
textObject.value.opacity !== undefined ? textObject.value.opacity : 1;
opacity.value = textObject.value.opacity !== undefined ? textObject.value.opacity : 1;
// 样式
fontWeight.value = textObject.value.fontWeight || "normal";
@@ -422,7 +398,7 @@ export default {
// 转换字符间距为百分比显示
charSpacingPercent.value = charSpacing.value / 10;
textSpacing.value = charSpacingPercent.value; // 暂用相同值
baseline.value = 0; // Fabric.js没有直接支持基线偏移用0初始化
// baseline.value = 0; // Fabric.js没有直接支持基线偏移用0初始化
};
const selectFont = (fontName) => {
@@ -459,8 +435,7 @@ export default {
// 颜色选择器相关功能
const openColorPicker = (mode) => {
colorPickerMode.value = mode;
currentColor.value =
mode === "text" ? textColor.value : backgroundColor.value;
currentColor.value = mode === "text" ? textColor.value : backgroundColor.value;
showColorPicker.value = true;
};
@@ -602,12 +577,12 @@ export default {
executeCommand(command);
};
// 基线更新 (Fabric.js没有直接支持可能需要自定义实现)
const updateBaseline = () => {
console.log("基线调整功能待实现", baseline.value);
// 注意Fabric.js 5没有直接支持基线调整
// 可能需要通过自定义处理或CSS方式实现
};
// // 基线更新 (Fabric.js没有直接支持可能需要自定义实现)
// const updateBaseline = () => {
// // console.log("基线调整功能待实现", baseline.value);
// // 注意Fabric.js 5没有直接支持基线调整
// // 可能需要通过自定义处理或CSS方式实现
// };
// 透明度更新
const updateOpacity = () => {
@@ -662,7 +637,7 @@ export default {
lineHeight,
charSpacingPercent,
textSpacing,
baseline,
// baseline,
showColorPicker,
colorPickerMode,
currentColor,
@@ -690,7 +665,7 @@ export default {
updateCharSpacing,
updateTextSpacing,
updateLineHeight,
updateBaseline,
// updateBaseline,
updateOpacity,
executeCommand,
};

View File

@@ -259,10 +259,7 @@ function handleKeyDown(event) {
const key = event.key.toUpperCase();
// 当处于输入状态时不触发快捷键
if (
event.target.tagName === "INPUT" ||
event.target.tagName === "TEXTAREA"
) {
if (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA") {
return;
}
@@ -312,10 +309,7 @@ const handleToolClick = (tool) => {
/>
<!-- 自定义工具栏按钮插槽 -->
<slot
name="customTools"
:tool-button-props="{ activeTool, canUndo, canRedo }"
/>
<slot name="customTools" :tool-button-props="{ activeTool, canUndo, canRedo }" />
</div>
</template>

View File

@@ -7,10 +7,7 @@
@touchstart.prevent="startTouchSliding"
@click="handleClick"
>
<div
class="slider-fill"
:style="{ height: `${displayPercentage}%` }"
></div>
<div class="slider-fill" :style="{ height: `${displayPercentage}%` }"></div>
<div
class="slider-thumb"
:style="{
@@ -154,9 +151,7 @@ function clearHideTooltipTimer() {
// 计算tooltip的可见性优先使用props中的showTooltip否则使用内部状态
const tooltipVisible = computed(() => {
return props.showTooltip !== undefined
? props.showTooltip
: internalShowTooltip.value;
return props.showTooltip !== undefined ? props.showTooltip : internalShowTooltip.value;
});
// 更新tooltip状态的方法
@@ -250,8 +245,7 @@ function updateValueFromMousePosition(event) {
}
} else {
// 检查是否可以进入吸附状态
const snapPercentage =
(props.snapThreshold / trackHeight) * (props.max - props.min);
const snapPercentage = (props.snapThreshold / trackHeight) * (props.max - props.min);
// 检查是否接近预设值,增加吸附判定范围
for (const preset of props.presets) {
@@ -417,8 +411,7 @@ function updateValueFromTouchPosition(event) {
}
} else {
// 检查是否可以进入吸附状态
const snapPercentage =
(props.snapThreshold / trackHeight) * (props.max - props.min);
const snapPercentage = (props.snapThreshold / trackHeight) * (props.max - props.min);
// 检查是否接近预设值,增加吸附判定范围
for (const preset of props.presets) {
@@ -500,9 +493,7 @@ onMounted(() => {
// 添加全局点击事件监听
if (props.closeOnOutsideClick) {
// 使用 setTimeout 确保点击事件在其他处理程序之后执行
window.addEventListener("click", (e) =>
setTimeout(() => handleOutsideClick(e), 0)
);
window.addEventListener("click", (e) => setTimeout(() => handleOutsideClick(e), 0));
}
});
@@ -616,7 +607,9 @@ onBeforeUnmount(() => {
background: rgba(255, 255, 255, 0.95);
border-radius: 10px;
padding: 10px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 3px 12px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
min-width: 120px;
z-index: 10;
@@ -664,7 +657,9 @@ onBeforeUnmount(() => {
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {

View File

@@ -34,14 +34,10 @@ import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面
import { LayerType, OperationType } from "./utils/layerHelper.js";
import { ToolManager } from "./managers/toolManager.js";
import { fabric } from "fabric-with-all";
import {
uploadImageAndCreateLayer,
loadImageUrlToLayer,
loadImage,
} from "./utils/imageHelper.js";
import { uploadImageAndCreateLayer, loadImageUrlToLayer, loadImage } from "./utils/imageHelper.js";
// import MinimapPanel from "./components/MinimapPanel.vue";
const KeyboardShortcutHelp = defineAsyncComponent(() =>
import("./components/KeyboardShortcutHelp.vue")
const KeyboardShortcutHelp = defineAsyncComponent(
() => import("./components/KeyboardShortcutHelp.vue")
);
const emit = defineEmits([
@@ -318,11 +314,7 @@ onMounted(async () => {
await layerManager.initializeLayers();
}
if (
props.enabledRedGreenMode &&
props.clothingImageUrl &&
props.redGreenImageUrl
) {
if (props.enabledRedGreenMode && props.clothingImageUrl && props.redGreenImageUrl) {
canvasManager.canvas.fill = "#fff"; // 设置画布背景色为白色 // 初始化红绿图模式管理器
redGreenModeManager = new RedGreenModeManager({
canvas: canvasManager.canvas,
@@ -369,10 +361,7 @@ onMounted(async () => {
console.error("更换底图失败:", error);
}
canvasManager?.centerBackgroundLayer?.(
canvasManager.canvas.width,
canvasManager.canvas.height
);
canvasManager?.centerBackgroundLayer?.(canvasManager.canvas.width, canvasManager.canvas.height);
}
// // 设置固定图层是否可擦除
@@ -395,7 +384,7 @@ onMounted(async () => {
requestAnimationFrame(() => {
setTimeout(() => {
// 初始状态下生成所有预览图
canvasManager.updateAllThumbnails();
canvasManager?.updateAllThumbnails?.();
}, 300);
});
});
@@ -559,9 +548,7 @@ function moveLayerDown(layerId) {
function removeLayer(layerId) {
// Check if this is the last layer - prevent deletion
if (layers.value.length <= 2) {
console.warn(
"Cannot delete the last layer. At least one layer must remain."
);
console.warn("Cannot delete the last layer. At least one layer must remain.");
return;
}
@@ -667,9 +654,7 @@ function handleLayersReorder(reorderData) {
const success = layerManager.reorderLayers(oldIndex, newIndex, layerId);
if (success) {
console.log(
`图层 ${layerId} 已从位置 ${oldIndex} 移动到位置 ${newIndex}`
);
console.log(`图层 ${layerId} 已从位置 ${oldIndex} 移动到位置 ${newIndex}`);
// 更新画布渲染顺序
if (canvasManager) {
@@ -686,12 +671,7 @@ function handleChildLayersReorder(reorderData) {
const { parentId, oldIndex, newIndex, layerId } = reorderData;
if (layerManager && layerManager.reorderChildLayers) {
const success = layerManager.reorderChildLayers(
parentId,
oldIndex,
newIndex,
layerId
);
const success = layerManager.reorderChildLayers(parentId, oldIndex, newIndex, layerId);
if (success) {
console.log(
@@ -851,8 +831,7 @@ defineExpose({
* @returns {Object} 优化结果统计
*/
optimizeLayerStructure() {
if (!layerManager)
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
if (!layerManager) return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
return layerManager.optimizeLayerStructure();
},
@@ -965,10 +944,7 @@ defineExpose({
<!-- <MinimapPanel v-if="minimapEnabled" :minimapManager="minimapManager" /> -->
<!-- 笔刷控制面板 -->
<BrushControlPanel
v-if="canvasManagerLoaded"
:activeTool="activeTool"
/>
<BrushControlPanel v-if="canvasManagerLoaded" :activeTool="activeTool" />
<!-- 文本编辑面板 -->
<TextEditorPanel
@@ -1001,11 +977,7 @@ defineExpose({
<div class="zoom-info">
缩放: {{ currentZoom }}%
<button class="reset-zoom" @click="resetZoom">重置视图</button>
<button
class="help-btn"
@click="toggleShortcutHelp"
title="查看快捷键和触控操作"
>
<button class="help-btn" @click="toggleShortcutHelp" title="查看快捷键和触控操作">
?
</button>
</div>
@@ -1045,11 +1017,7 @@ defineExpose({
</div> -->
<!-- 快捷键帮助模态框 -->
<div
v-if="showShortcutHelp"
class="modal-overlay"
@click="showShortcutHelp = false"
>
<div v-if="showShortcutHelp" class="modal-overlay" @click="showShortcutHelp = false">
<div class="modal-content" @click.stop>
<button class="close-modal" @click="showShortcutHelp = false">×</button>
<KeyboardShortcutHelp />
@@ -1126,30 +1094,20 @@ defineExpose({
--offsetY: 0px;
--size: 8px;
--color: #dedcdc;
background-image: -webkit-linear-gradient(
background-image:
-webkit-linear-gradient(
45deg,
var(--color) 25%,
transparent 0,
transparent 75%,
var(--color) 0
),
-webkit-linear-gradient(45deg, var(--color) 25%, transparent 0, transparent
75%, var(--color) 0);
background-image: linear-gradient(
45deg,
var(--color) 25%,
transparent 0,
transparent 75%,
var(--color) 0
),
linear-gradient(
45deg,
var(--color) 25%,
transparent 0,
transparent 75%,
var(--color) 0
);
background-position: var(--offsetX) var(--offsetY),
-webkit-linear-gradient(45deg, var(--color) 25%, transparent 0, transparent 75%, var(--color) 0);
background-image:
linear-gradient(45deg, var(--color) 25%, transparent 0, transparent 75%, var(--color) 0),
linear-gradient(45deg, var(--color) 25%, transparent 0, transparent 75%, var(--color) 0);
background-position:
var(--offsetX) var(--offsetY),
calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
background-size: calc(var(--size) * 2) calc(var(--size) * 2);
}
@@ -1369,7 +1327,9 @@ button:hover {
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {

View File

@@ -97,8 +97,7 @@ export class BrushIndicator {
if (!this.staticCanvas || !this.canvas) return;
// 检查是否为笔刷或橡皮擦模式,非相关模式直接返回
const isBrushMode =
this.canvas.isDrawingMode && this.canvas.freeDrawingBrush;
const isBrushMode = this.canvas.isDrawingMode && this.canvas.freeDrawingBrush;
const isEraserMode =
this.canvas.isDrawingMode &&
this.canvas.freeDrawingBrush &&
@@ -136,10 +135,7 @@ export class BrushIndicator {
const currentVpt = this.canvas.viewportTransform;
if (
currentVpt &&
!this._areViewportTransformsEqual(
currentVpt,
this._lastCanvasState.viewportTransform
)
!this._areViewportTransformsEqual(currentVpt, this._lastCanvasState.viewportTransform)
) {
this.staticCanvas.setViewportTransform([...currentVpt]);
this._lastCanvasState.viewportTransform = [...currentVpt];

View File

@@ -1,7 +1,5 @@
import { fabric } from "fabric-with-all";
import initAligningGuidelines, {
initCenteringGuidelines,
} from "../utils/helperLine";
import initAligningGuidelines, { initCenteringGuidelines } from "../utils/helperLine";
import { ThumbnailManager } from "./ThumbnailManager";
import { ExportManager } from "./ExportManager";
import {
@@ -16,11 +14,7 @@ import { CanvasEventManager } from "./events/CanvasEventManager";
import CanvasConfig from "../config/canvasConfig";
import { RedGreenModeManager } from "./RedGreenModeManager";
import { EraserStateManager } from "./EraserStateManager";
import {
deepClone,
generateId,
optimizeCanvasRendering,
} from "../utils/helper";
import { deepClone, generateId, optimizeCanvasRendering } from "../utils/helper";
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
import { isFunction } from "lodash-es";
import {
@@ -155,10 +149,7 @@ export class CanvasManager {
return false;
};
this.eraserStateManager = new EraserStateManager(
this.canvas,
this.layerManager
);
this.eraserStateManager = new EraserStateManager(this.canvas, this.layerManager);
// 监听擦除开始事件
this.canvas.on("erasing:start", () => {
@@ -179,17 +170,12 @@ export class CanvasManager {
}
// 更新交互性
command &&
(await this.layerManager?.updateLayersObjectsInteractivity?.());
command && (await this.layerManager?.updateLayersObjectsInteractivity?.());
this.thumbnailManager?.generateLayerThumbnail(
this.layerManager?.activeLayerId?.value
);
this.thumbnailManager?.generateLayerThumbnail(this.layerManager?.activeLayerId?.value);
// 固定图层 的擦除也需要重新生成缩略图 要判断 当前固定图层是否锁定
const fixedLayer = this.layers?.value?.find(
(layer) => layer.isFixed && !layer.locked
);
const fixedLayer = this.layers?.value?.find((layer) => layer.isFixed && !layer.locked);
// 如果有固定图层且未锁定,则生成缩略图
fixedLayer &&
this.isFixedErasable &&
@@ -378,9 +364,7 @@ export class CanvasManager {
// 设置固定图层的可擦除状态
layer.locked = flag;
// 更新画布对象的erasable属性
const fabricObject = this.canvas
.getObjects()
.find((obj) => obj.id === layer.id);
const fabricObject = this.canvas.getObjects().find((obj) => obj.id === layer.id);
if (fabricObject) {
fabricObject.erasable = flag;
fabricObject.set("erasable", flag);
@@ -492,9 +476,7 @@ export class CanvasManager {
const deltaY = backgroundObject.top - backgroundOldTop;
// 将相同的偏移量应用到所有其他对象上
const otherObjects = visibleObjects.filter(
(obj) => obj !== backgroundObject
);
const otherObjects = visibleObjects.filter((obj) => obj !== backgroundObject);
otherObjects.forEach((obj) => {
obj.set({
@@ -597,8 +579,7 @@ export class CanvasManager {
if (!backgroundLayerObject) return;
const bgWidth = backgroundLayerObject.width * backgroundLayerObject.scaleX;
const bgHeight =
backgroundLayerObject.height * backgroundLayerObject.scaleY;
const bgHeight = backgroundLayerObject.height * backgroundLayerObject.scaleY;
const left = backgroundLayerObject.left;
const top = backgroundLayerObject.top;
@@ -659,9 +640,7 @@ export class CanvasManager {
return obj.isBackground || obj.id === backgroundLayerId;
});
if (!backgroundLayerByBgLayer) {
console.warn(
"CanvasManager.js = >getBackgroundLayer 方法没有找到背景层"
);
console.warn("CanvasManager.js = >getBackgroundLayer 方法没有找到背景层");
}
return backgroundLayerByBgLayer;
@@ -671,8 +650,7 @@ export class CanvasManager {
* @param {Object} backgroundLayerObject 背景层对象
*/
updateMaskPosition(backgroundLayerObject) {
if (!backgroundLayerObject || !this.maskLayer || !this.canvas.clipPath)
return;
if (!backgroundLayerObject || !this.maskLayer || !this.canvas.clipPath) return;
const left = backgroundLayerObject.left;
const top = backgroundLayerObject.top;
@@ -737,8 +715,7 @@ export class CanvasManager {
...options,
});
command.undoable =
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
command.undoable = options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
const result = (await command?.execute?.()) || {
success: false,
@@ -785,9 +762,7 @@ export class CanvasManager {
...options,
// 如果没有明确指定,则根据当前模式自动设置
restoreOpacityInRedGreen:
options.restoreOpacityInRedGreen !== undefined
? options.restoreOpacityInRedGreen
: true, // 默认在红绿图模式下恢复透明度
options.restoreOpacityInRedGreen !== undefined ? options.restoreOpacityInRedGreen : true, // 默认在红绿图模式下恢复透明度
};
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
@@ -801,9 +776,7 @@ export class CanvasManager {
// 获取所有非背景、非固定的普通图层ID
const normalLayerIds =
this.layers?.value
?.filter(
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
)
?.filter((layer) => !layer.isBackground && !layer.isFixed && layer.visible)
?.map((layer) => layer.id) || [];
if (normalLayerIds.length > 0) {
@@ -912,9 +885,11 @@ export class CanvasManager {
// });
// };
try {
const simplifyLayersData = simplifyLayers(
JSON.parse(JSON.stringify(this.layers.value))
);
// 清除画布中选中状态
this.canvas.discardActiveObject();
this.canvas.renderAll();
const simplifyLayersData = simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)));
console.log("获取画布JSON数据...", simplifyLayersData);
return JSON.stringify({
canvas: this.canvas.toJSON([
@@ -962,6 +937,7 @@ export class CanvasManager {
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const tempLayers = JSON.parse(parsedJson?.layers) || [];
const canvasData = parsedJson?.canvas;
@@ -1023,10 +999,7 @@ export class CanvasManager {
// 重置画布数据
this.setCanvasSize(this.canvas.width, this.canvas.height);
// 重新构建对象关系
restoreObjectLayerAssociations(
this.layers.value,
this.canvas.getObjects()
);
restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
// 验证图层关联关系 - 稳定后可以注释
const isValidate = validateLayerAssociations(
this.layers.value,
@@ -1038,8 +1011,7 @@ export class CanvasManager {
// 使用LayerSort工具重新排列画布对象如果可用
await this?.layerManager?.layerSort?.rearrangeObjects();
this.layerManager.activeLayerId.value = this.layers.value[0]
.children?.length
this.layerManager.activeLayerId.value = this.layers.value[0].children?.length
? this.layers.value[0].children[0].id
: this.layers.value[0]?.id || parsedJson?.activeLayerId || null;
@@ -1052,9 +1024,7 @@ export class CanvasManager {
await calllBack?.();
// 确保所有对象的交互性正确设置
await this.layerManager?.updateLayersObjectsInteractivity?.(
false
);
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
console.log(this.layerManager.layers.value);
// 更新所有缩略图
@@ -1211,9 +1181,7 @@ export class CanvasManager {
if (!this.layers || !this.layers.value) return [];
// 查找所有非背景、非固定的普通图层
const normalLayers = this.layers.value.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
const normalLayers = this.layers.value.filter((layer) => !layer.isBackground && !layer.isFixed);
// 收集所有普通图层中的对象
const objects = [];
@@ -1250,15 +1218,9 @@ export class CanvasManager {
// 比较尺寸允许5%的误差)
const sizeMatch =
Math.abs(
obj.width * obj.scaleX -
fixedLayerObject.width * fixedLayerObject.scaleX
) <
Math.abs(obj.width * obj.scaleX - fixedLayerObject.width * fixedLayerObject.scaleX) <
fixedLayerObject.width * fixedLayerObject.scaleX * 0.05 &&
Math.abs(
obj.height * obj.scaleY -
fixedLayerObject.height * fixedLayerObject.scaleY
) <
Math.abs(obj.height * obj.scaleY - fixedLayerObject.height * fixedLayerObject.scaleY) <
fixedLayerObject.height * fixedLayerObject.scaleY * 0.05;
// 比较位置(允许一定的偏差)

View File

@@ -86,12 +86,7 @@ export class ExportManager {
* @returns {String} 图片数据URL
* @private
*/
_exportSpecificLayer(
layerId,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
) {
_exportSpecificLayer(layerId, expPicType, isRedGreenMode, restoreOpacityInRedGreen) {
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
}
@@ -115,19 +110,11 @@ export class ExportManager {
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen);
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen);
}
/**
@@ -168,19 +155,11 @@ export class ExportManager {
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen);
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen);
}
/**
@@ -215,22 +194,14 @@ export class ExportManager {
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen);
}
let canvasClipPath = this.canvas.clipPath;
if (isCropByBg) {
const cropWidth =
this.canvasManager?.canvasWidth?.value ||
this.canvas?.canvasWidth ||
this.canvas.width;
this.canvasManager?.canvasWidth?.value || this.canvas?.canvasWidth || this.canvas.width;
const cropHeight =
this.canvasManager?.canvasHeight?.value ||
this.canvas?.canvasHeight ||
this.canvas.height;
this.canvasManager?.canvasHeight?.value || this.canvas?.canvasHeight || this.canvas.height;
canvasClipPath = new fabric.Rect({
left: this.canvas.width / 2,
top: this.canvas.height / 2,
@@ -331,27 +302,14 @@ export class ExportManager {
* @returns {String} 图片数据URL
* @private
*/
async _exportObject(
obj,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
) {
async _exportObject(obj, expPicType, isRedGreenMode, restoreOpacityInRedGreen) {
// 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) {
return this._exportWithRedGreenMode(
[obj],
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithRedGreenMode([obj], expPicType, restoreOpacityInRedGreen);
}
// 普通模式使用画布尺寸
return this._exportWithCanvasSize(
[obj],
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithCanvasSize([obj], expPicType, restoreOpacityInRedGreen);
}
/**
@@ -374,8 +332,7 @@ export class ExportManager {
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
// 检查图层类型过滤条件
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed))
continue;
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) continue;
if (layer.visible) {
const layerObjects = this._collectObjectsFromLayer(layer);
@@ -514,21 +471,12 @@ export class ExportManager {
* @returns {String} 图片数据URL
* @private
*/
async _exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
) {
async _exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen) {
// 获取固定图层对象(衣服底图)作为参考
const fixedLayerObject =
this._getFixedLayerObject() ?? this.canvas.clipPath;
const fixedLayerObject = this._getFixedLayerObject() ?? this.canvas.clipPath;
if (!fixedLayerObject) {
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸");
return this._exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen);
}
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
@@ -565,10 +513,7 @@ export class ExportManager {
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
for (let i = 0; i < objectsToExport.length; i++) {
const obj = objectsToExport[i];
const cloned = await this._cloneObjectForExport(
obj,
restoreOpacityInRedGreen && true
);
const cloned = await this._cloneObjectForExport(obj, restoreOpacityInRedGreen && true);
if (cloned) {
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
cloned.set({
@@ -606,12 +551,7 @@ export class ExportManager {
* @returns {String} 图片数据URL
* @private
*/
async _exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen,
maskObject
) {
async _exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen, maskObject) {
// 使用当前画布尺寸
// const canvasWidth =
// this.canvasManager?.canvasWidth?.value || this.canvas.width;
@@ -634,44 +574,44 @@ export class ExportManager {
console.log("导出图片数据URL:", dataURL);
return dataURL;
// 创建与画布相同尺寸的临时画布
const scaleFactor = 2; // 高清导出
const tempCanvas = document.createElement("canvas");
tempCanvas.width = canvasWidth * scaleFactor;
tempCanvas.height = canvasHeight * scaleFactor;
tempCanvas.style.width = canvasWidth + "px";
tempCanvas.style.height = canvasHeight + "px";
// // 创建与画布相同尺寸的临时画布
// const scaleFactor = 2; // 高清导出
// const tempCanvas = document.createElement("canvas");
// tempCanvas.width = canvasWidth * scaleFactor;
// tempCanvas.height = canvasHeight * scaleFactor;
// tempCanvas.style.width = canvasWidth + "px";
// tempCanvas.style.height = canvasHeight + "px";
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
width: canvasWidth,
height: canvasHeight,
backgroundColor: null,
});
// const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
// width: canvasWidth,
// height: canvasHeight,
// backgroundColor: null,
// });
tempFabricCanvas.enableRetinaScaling = true;
tempFabricCanvas.imageSmoothingEnabled = true;
tempFabricCanvas.setZoom(1);
// tempFabricCanvas.enableRetinaScaling = true;
// tempFabricCanvas.imageSmoothingEnabled = true;
// tempFabricCanvas.setZoom(1);
try {
// 克隆并添加所有对象到临时画布
for (const obj of objectsToExport) {
const cloned = await this._cloneObjectForExport(
obj,
restoreOpacityInRedGreen && false // 普通模式不强制恢复透明度
);
if (cloned) {
tempFabricCanvas.add(cloned);
}
}
// try {
// // 克隆并添加所有对象到临时画布
// for (const obj of objectsToExport) {
// const cloned = await this._cloneObjectForExport(
// obj,
// restoreOpacityInRedGreen && false, // 普通模式不强制恢复透明度
// );
// if (cloned) {
// tempFabricCanvas.add(cloned);
// }
// }
// 渲染画布
tempFabricCanvas.renderAll();
// // 渲染画布
// tempFabricCanvas.renderAll();
// 生成图片
return this._generateHighQualityDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
// // 生成图片
// return this._generateHighQualityDataURL(tempCanvas, expPicType);
// } finally {
// this._cleanupTempCanvas(tempFabricCanvas);
// }
}
/**
@@ -703,10 +643,7 @@ export class ExportManager {
* @returns {Promise<fabric.Object>} 克隆的对象
* @private
*/
_cloneObjectAsync(
obj,
propertiesToInclude = ["id", "layerId", "layerName", "name"]
) {
_cloneObjectAsync(obj, propertiesToInclude = ["id", "layerId", "layerName", "name"]) {
return new Promise((resolve, reject) => {
if (!obj) {
resolve(null);
@@ -736,11 +673,7 @@ export class ExportManager {
* @returns {Promise<Object>} 克隆的对象
* @private
*/
async _cloneObjectForExport(
obj,
forceRestoreOpacity = false,
removeClipPath = true
) {
async _cloneObjectForExport(obj, forceRestoreOpacity = false, removeClipPath = true) {
if (!obj) return null;
try {
@@ -874,11 +807,7 @@ export class ExportManager {
}
// 克隆对象作为裁剪路径
const clonedClipPath = await this._cloneObjectForExport(
clipObject,
false,
false
);
const clonedClipPath = await this._cloneObjectForExport(clipObject, false, false);
if (!clonedClipPath) {
console.warn("无法克隆裁剪对象");

View File

@@ -51,24 +51,16 @@ import {
} from "../commands/RasterizeLayerCommand";
// 导入图层排序相关类和混入
import {
LayerSort,
createLayerSort,
LayerSortMixin,
LayerSortUtils,
} from "../utils/LayerSort";
import { LayerSort, createLayerSort, LayerSortMixin, LayerSortUtils } from "../utils/LayerSort";
import CanvasConfig from "../config/canvasConfig";
import { isBoolean, template } from "lodash-es";
import {
findObjectById,
generateId,
optimizeCanvasRendering,
} from "../utils/helper";
import { findObjectById, generateId, optimizeCanvasRendering } from "../utils/helper";
import { message } from "ant-design-vue";
import { fabric } from "fabric-with-all";
import { getOriginObjectInfo } from "../utils/layerUtils";
import { restoreFabricObject } from "../utils/objectHelper";
import { UpdateGroupMaskPositionCommand } from "../commands/UpdateGroupMaskPositionCommand";
/**
* 图层管理器 - 负责管理画布上的所有图层
@@ -219,11 +211,7 @@ export class LayerManager {
// 基于 fabric-with-erasing 库的 erasable 属性设置擦除权限
// 只有活动图层、可见、非锁定、非背景、非固定图层的对象才可擦除
obj.erasable =
isInActiveLayer &&
layer.visible &&
!layer.locked &&
!layer.isBackground &&
!layer.isFixed;
isInActiveLayer && layer.visible && !layer.locked && !layer.isBackground && !layer.isFixed;
// 图层状态决定交互性
if (layer.isBackground || obj.isBackground || layer.isFixed) {
@@ -270,11 +258,7 @@ export class LayerManager {
if (this.isRedGreenMode) {
// 红绿图模式下 所有普通图层都可擦除
obj.erasable =
layer.visible &&
!layer.locked &&
!layer.isBackground &&
!layer.isFixed;
obj.erasable = layer.visible && !layer.locked && !layer.isBackground && !layer.isFixed;
}
// 平移模式下,禁用多选和擦除
@@ -325,9 +309,7 @@ export class LayerManager {
// 设置子图层对象的交互性
layer?.childLayer?.forEach(async (childLayer) => {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
const childObj = this.canvas.getObjects().find((o) => o.layerId === childLayer.id);
if (childObj) {
await this._setObjectInteractivity(childObj, childLayer, editorMode);
}
@@ -339,10 +321,7 @@ export class LayerManager {
let clippingMaskFabricObject = null;
if (layer.clippingMask) {
// 反序列化 clippingMask
clippingMaskFabricObject = await restoreFabricObject(
layer.clippingMask,
this.canvas
);
clippingMaskFabricObject = await restoreFabricObject(layer.clippingMask, this.canvas);
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
@@ -354,9 +333,7 @@ export class LayerManager {
// 如果是组图层 则给所有子对象设置裁剪对象
if (layer.type === LayerType.GROUP || layer.children?.length > 0) {
layer.children.forEach((childLayer) => {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
const childObj = this.canvas.getObjects().find((o) => o.layerId === childLayer.id);
if (childObj) {
childObj.clipPath = clippingMaskFabricObject;
childObj.dirty = true; // 标记为脏对象
@@ -365,9 +342,7 @@ export class LayerManager {
});
} else {
layer.fabricObjects?.forEach((obj) => {
const fabricObject = this.canvas
.getObjects()
.find((o) => o.id === obj.id);
const fabricObject = this.canvas.getObjects().find((o) => o.id === obj.id);
if (fabricObject) {
fabricObject.clipPath = clippingMaskFabricObject;
fabricObject.dirty = true; // 标记为脏对象
@@ -447,9 +422,7 @@ export class LayerManager {
*/
createBackgroundLayer(name = "背景") {
// 检查是否已有背景图层
const hasBackgroundLayer = this.layers.value.some(
(layer) => layer.isBackground
);
const hasBackgroundLayer = this.layers.value.some((layer) => layer.isBackground);
if (hasBackgroundLayer) {
console.warn("已存在背景层,不再创建新的背景层");
@@ -502,9 +475,7 @@ export class LayerManager {
}
// 生成唯一ID
const layerId = `fixed_layer_${Date.now()}_${Math.floor(
Math.random() * 1000
)}`;
const layerId = `fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
// 创建固定图层
const fixedLayer = createFixedLayer({
@@ -564,9 +535,7 @@ export class LayerManager {
await this.createLayer("图层 1");
} else {
// 检查是否已有背景层
const hasBackgroundLayer = this.layers.value.some(
(layer) => layer.isBackground
);
const hasBackgroundLayer = this.layers.value.some((layer) => layer.isBackground);
if (!hasBackgroundLayer) {
this.createBackgroundLayer();
@@ -612,10 +581,7 @@ export class LayerManager {
}
// 验证目标图层是否存在
const { layer: targetLayer } = findLayerRecursively(
this.layers.value,
targetLayerId
);
const { layer: targetLayer } = findLayerRecursively(this.layers.value, targetLayerId);
if (!targetLayer) {
console.error(`目标图层 ${targetLayerId} 不存在`);
return null;
@@ -669,8 +635,7 @@ export class LayerManager {
*/
removeObjectFromLayer(objectOrId) {
// 获取对象ID
const objectId =
typeof objectOrId === "string" ? objectOrId : objectOrId.id;
const objectId = typeof objectOrId === "string" ? objectOrId : objectOrId.id;
if (!objectId) {
console.error("无效的对象ID");
@@ -729,9 +694,7 @@ export class LayerManager {
*/
getActiveLayer() {
if (!this.activeLayerId.value) {
console.warn(
"没有活动图层ID无法获取活动图层 ==== 默认设置第一个图层为活动图层"
);
console.warn("没有活动图层ID无法获取活动图层 ==== 默认设置第一个图层为活动图层");
this.activeLayerId.value = this.layers.value[0]?.id || null;
}
@@ -836,13 +799,8 @@ export class LayerManager {
}
const tempFabricObject =
fabricObject?.toObject?.([
"id",
"layerId",
"layerName",
"isBackgroud",
"isFixed",
]) || fabricObject;
fabricObject?.toObject?.(["id", "layerId", "layerName", "isBackgroud", "isFixed"]) ||
fabricObject;
if (layer.isFixed || layer.isBackground) {
layer.fabricObject = tempFabricObject;
} else {
@@ -868,9 +826,7 @@ export class LayerManager {
// 如果是背景层或固定层,不允许移动
if (layer && (layer.isBackground || layer.isFixed)) {
console.warn(
layer.isBackground ? $t("背景层不可移动") : $t("固定层不可移动")
);
console.warn(layer.isBackground ? $t("背景层不可移动") : $t("固定层不可移动"));
return false;
}
@@ -1000,6 +956,9 @@ export class LayerManager {
// 设置活动选择组的属性
this.canvas.setActiveObject(activeSelection);
// 为活动选择组添加移动事件监听器,用于同步更新遮罩位置
this._setupGroupMaskMovementSync(activeSelection, layer);
activeSelection = null; // 清理引用,避免内存泄漏
// 确保选择组正确渲染
// activeSelection.setCoords();
@@ -1234,11 +1193,7 @@ export class LayerManager {
async ungroupLayers(groupId) {
// 查找组图层
const groupLayer = this.layers.value.find((l) => l.id === groupId);
if (
!groupLayer ||
!groupLayer.children ||
groupLayer.children.length === 0
) {
if (!groupLayer || !groupLayer.children || groupLayer.children.length === 0) {
console.error(`${groupId} 不是有效的组图层或不包含子图层`);
return null;
}
@@ -1295,9 +1250,7 @@ export class LayerManager {
*/
resizeCanvasWithScale(width, height, options = {}) {
// 检查是否有除背景层外的其他元素
const hasOtherElements = this.canvas
.getObjects()
.some((obj) => !obj.isBackground);
const hasOtherElements = this.canvas.getObjects().some((obj) => !obj.isBackground);
if (hasOtherElements) {
// 有其他元素时使用带缩放的命令
@@ -1366,9 +1319,7 @@ export class LayerManager {
if (!this.canvas) return;
// 获取画布上的所有对象
const canvasObjects = [
...this.canvas.getObjects(["id", "layerId", "layerName"]),
];
const canvasObjects = [...this.canvas.getObjects(["id", "layerId", "layerName"])];
// 清空画布
this.canvas.clear();
@@ -1383,19 +1334,13 @@ export class LayerManager {
if (layer.isBackground && layer.fabricObject) {
// 背景图层
const originalObj = canvasObjects.find(
(o) => o.id === layer.fabricObject.id
);
const originalObj = canvasObjects.find((o) => o.id === layer.fabricObject.id);
if (originalObj) {
this.canvas.add(originalObj);
} else {
this.canvas.add(layer.fabricObject);
}
} else if (
layer.isFixed &&
layer.fabricObjects &&
layer.fabricObjects.length > 0
) {
} else if (layer.isFixed && layer.fabricObjects && layer.fabricObjects.length > 0) {
// 固定图层
layer.fabricObjects.forEach((obj) => {
const originalObj = canvasObjects.find((o) => o.id === obj.id);
@@ -1405,10 +1350,7 @@ export class LayerManager {
this.canvas.add(obj);
}
});
} else if (
Array.isArray(layer.fabricObjects) &&
layer.fabricObjects.length > 0
) {
} else if (Array.isArray(layer.fabricObjects) && layer.fabricObjects.length > 0) {
// 普通图层添加所有fabricObjects
layer.fabricObjects.forEach((obj) => {
const originalObj = canvasObjects.find((o) => o.id === obj.id);
@@ -1437,9 +1379,7 @@ export class LayerManager {
if (layer.isBackground) {
// 背景图层处理
if (layer.fabricObject) {
const existsOnCanvas = canvasObjects.some(
(obj) => obj.id === layer.fabricObject.id
);
const existsOnCanvas = canvasObjects.some((obj) => obj.id === layer.fabricObject.id);
if (!existsOnCanvas) {
this.canvas.add(layer.fabricObject);
}
@@ -1549,9 +1489,7 @@ export class LayerManager {
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
layerCopy.serializedObjects = layer.fabricObjects
.map((obj) =>
typeof obj.toObject === "function"
? obj.toObject(["id", "layerId", "layerName"])
: null
typeof obj.toObject === "function" ? obj.toObject(["id", "layerId", "layerName"]) : null
)
.filter(Boolean);
}
@@ -1596,9 +1534,7 @@ export class LayerManager {
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
layerCopy.serializedObjects = layer.fabricObjects
.map((obj) =>
typeof obj.toObject === "function"
? obj.toObject(["id", "layerId", "layerName"])
: null
typeof obj.toObject === "function" ? obj.toObject(["id", "layerId", "layerName"]) : null
)
.filter(Boolean);
}
@@ -1719,8 +1655,7 @@ export class LayerManager {
// 查找第一个非背景、非锁定的图层,排除指定的图层
return (
this.layers.value.find(
(layer) =>
layer.id !== excludeLayerId && !layer.isBackground && !layer.locked
(layer) => layer.id !== excludeLayerId && !layer.isBackground && !layer.locked
) || null
);
}
@@ -1855,9 +1790,7 @@ export class LayerManager {
* @param {string} backgroundColor 背景颜色
*/
updateBackgroundColor(backgroundColor, options = {}) {
const backgroundLayer = this.layers.value.find(
(layer) => layer.isBackground
);
const backgroundLayer = this.layers.value.find((layer) => layer.isBackground);
if (!backgroundLayer) {
console.warn("没有找到背景图层");
@@ -2057,8 +1990,7 @@ export class LayerManager {
if (!this.canvas || !textObject) return null;
// 确保对象有ID
textObject.id =
textObject.id || `text_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
textObject.id = textObject.id || `text_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
// 创建文本图层
const layerName = options.name || "文本图层";
@@ -2075,9 +2007,7 @@ export class LayerManager {
overline: options.overline || textObject.overline || false,
fill: options.fill || textObject.fill || "#000000",
textBackgroundColor:
options.textBackgroundColor ||
textObject.textBackgroundColor ||
"transparent",
options.textBackgroundColor || textObject.textBackgroundColor || "transparent",
lineHeight: options.lineHeight || textObject.lineHeight || 1.16,
charSpacing: options.charSpacing || textObject.charSpacing || 0,
},
@@ -2088,9 +2018,7 @@ export class LayerManager {
textObject.layerName = layerName;
// 添加到画布,如果还未添加
const isOnCanvas = this.canvas
.getObjects()
.some((obj) => obj.id === textObject.id);
const isOnCanvas = this.canvas.getObjects().some((obj) => obj.id === textObject.id);
if (!isOnCanvas) {
this.canvas.add(textObject);
}
@@ -2099,9 +2027,7 @@ export class LayerManager {
const layer = this.getLayerById(layerId);
if (layer) {
layer.fabricObjects = layer.fabricObjects || [];
layer.fabricObjects.push(
textObject.toObject(["id", "layerId", "layerName"])
);
layer.fabricObjects.push(textObject.toObject(["id", "layerId", "layerName"]));
}
// 设置此图层为活动图层
@@ -2134,9 +2060,7 @@ export class LayerManager {
// 检查普通图层
if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
const foundObject = layer.fabricObjects.find(
(obj) => obj.id === fabricObject.id
);
const foundObject = layer.fabricObjects.find((obj) => obj.id === fabricObject.id);
if (foundObject) {
return layer;
}
@@ -2217,12 +2141,7 @@ export class LayerManager {
* @returns {boolean} 是否排序成功
*/
reorderChildLayers(parentId, oldIndex, newIndex, layerId) {
return this.layerSort?.reorderChildLayers(
parentId,
oldIndex,
newIndex,
layerId
);
return this.layerSort?.reorderChildLayers(parentId, oldIndex, newIndex, layerId);
}
/**
@@ -2403,19 +2322,13 @@ export class LayerManager {
const layersCount = this.layers?.value?.length || 0;
const shouldUseAsync =
async !== null
? async
: LayerSortUtils.shouldUseAsyncProcessing(objectsCount, layersCount);
async !== null ? async : LayerSortUtils.shouldUseAsyncProcessing(objectsCount, layersCount);
if (shouldUseAsync) {
console.log(
`使用异步排序处理 ${objectsCount} 个对象, ${layersCount} 个图层`
);
console.log(`使用异步排序处理 ${objectsCount} 个对象, ${layersCount} 个图层`);
return this.layerSort.rearrangeObjectsAsync();
} else {
console.log(
`使用同步排序处理 ${objectsCount} 个对象, ${layersCount} 个图层`
);
console.log(`使用同步排序处理 ${objectsCount} 个对象, ${layersCount} 个图层`);
this.layerSort.rearrangeObjects();
}
}
@@ -2524,10 +2437,7 @@ export class LayerManager {
moveLayerToIndex({ parentId, oldIndex, newIndex, layerId }) {
if (!this.layerSort) {
console.warn("图层排序工具未初始化,使用基础移动方法");
return this.moveLayer(
layerId,
newIndex > this.getLayerIndex(layerId) ? "down" : "up"
);
return this.moveLayer(layerId, newIndex > this.getLayerIndex(layerId) ? "down" : "up");
}
const result = this.layerSort.moveLayerToIndex({
@@ -2538,9 +2448,7 @@ export class LayerManager {
});
if (result) {
console.log(
`图层 ${layerId} - oldIndex: ${oldIndex} 已移动到位置 ${newIndex}`
);
console.log(`图层 ${layerId} - oldIndex: ${oldIndex} 已移动到位置 ${newIndex}`);
// 更新对象交互性
// this.updateLayersObjectsInteractivity();
}
@@ -2584,9 +2492,7 @@ export class LayerManager {
return -1;
}
let layerIndex = this.layers.value.findIndex(
(layer) => layer.id === layerId
);
let layerIndex = this.layers.value.findIndex((layer) => layer.id === layerId);
if (layerIndex >= 0) return layerIndex;
// 如果未找到,尝试在子图层中查找
const { parent } = findLayerRecursively(this.layers.value, layerId);
@@ -2617,9 +2523,7 @@ export class LayerManager {
const result = this.layerSort.reorderLayers(oldIndex, newIndex, layerId);
if (result) {
console.log(
`高级排序完成: 图层 ${layerId} 从位置 ${oldIndex} 移动到 ${newIndex}`
);
console.log(`高级排序完成: 图层 ${layerId} 从位置 ${oldIndex} 移动到 ${newIndex}`);
// 更新对象交互性
this.updateLayersObjectsInteractivity();
}
@@ -2636,12 +2540,7 @@ export class LayerManager {
* @returns {boolean} 是否排序成功
*/
advancedReorderChildLayers(parentId, oldIndex, newIndex, layerId) {
const result = this.reorderChildLayers(
parentId,
oldIndex,
newIndex,
layerId
);
const result = this.reorderChildLayers(parentId, oldIndex, newIndex, layerId);
if (result) {
console.log(
@@ -2758,11 +2657,7 @@ export class LayerManager {
async mergeGroupLayers(groupId) {
// 查找组图层
const groupLayer = this.layers.value.find((l) => l.id === groupId);
if (
!groupLayer ||
!groupLayer.children ||
groupLayer.children.length === 0
) {
if (!groupLayer || !groupLayer.children || groupLayer.children.length === 0) {
console.warn($t("找不到有效的组图层或组图层为空"));
return [];
}
@@ -2803,10 +2698,7 @@ export class LayerManager {
// 查找目标图层
// const targetLayer = this.getLayerById(targetLayerId);
const { layer: targetLayer } = findLayerRecursively(
this.layers.value,
targetLayerId
);
const { layer: targetLayer } = findLayerRecursively(this.layers.value, targetLayerId);
if (!targetLayer) {
console.error($t("图层不存在", { layerId: targetLayerId }));
@@ -2864,9 +2756,7 @@ export class LayerManager {
return (
layer.children &&
layer.children.length > 0 &&
layer.children.some(
(child) => child.fabricObjects && child.fabricObjects.length > 0
)
layer.children.some((child) => child.fabricObjects && child.fabricObjects.length > 0)
);
} else {
// 普通图层:检查是否有对象
@@ -2888,10 +2778,7 @@ export class LayerManager {
// 查找目标图层
// const targetLayer = this.getLayerById(targetLayerId);
const { layer: targetLayer } = findLayerRecursively(
this.layers.value,
targetLayerId
);
const { layer: targetLayer } = findLayerRecursively(this.layers.value, targetLayerId);
if (!targetLayer) {
console.error($t("图层不存在", { layerId: targetLayerId }));
@@ -2926,15 +2813,162 @@ export class LayerManager {
}
/**
* 更新图层缩略图
* @param {string} layerId 图层ID
* 为组图层的活动选择组设置遮罩移动同步(修复版)
* @param {fabric.ActiveSelection} activeSelection 活动选择组
* @param {Object} layer 组图层对象
* @private
*/
_updateLayerThumbnail(layerId) {
if (this.canvas && this.canvas.thumbnailManager) {
setTimeout(() => {
this.canvas.thumbnailManager.generateLayerThumbnail(layerId);
}, 100);
_setupGroupMaskMovementSync(activeSelection, layer) {
if (!activeSelection || !layer || !layer.clippingMask) {
return;
}
// 记录初始位置
let initialLeft = activeSelection.left;
let initialTop = activeSelection.top;
// 记录遮罩初始位置
let maskInitialLeft = layer.clippingMask.left || 0;
let maskInitialTop = layer.clippingMask.top || 0;
// 用于节流和状态管理的变量
let isUpdating = false;
let lastUpdateTime = 0;
let hasMoved = false; // 追踪是否实际发生了移动
const UPDATE_THRESHOLD = 16; // 约60fps
// 移动开始事件处理
const handleMovingStart = (e) => {
if (e.target === activeSelection) {
hasMoved = false; // 重置移动状态
console.log("🎯 开始移动组选择对象");
// 记录遮罩初始位置
console.log(
"🖼️ 记录遮罩初始位置",
`${layer.clippingMask.left || 0}, ${layer.clippingMask.top || 0}`
);
// 记录初始位置
initialLeft = activeSelection.left;
initialTop = activeSelection.top;
maskInitialLeft = layer.clippingMask.left || 0;
maskInitialTop = layer.clippingMask.top || 0;
}
};
// 移动中事件处理函数(带节流)
const handleMoving = (e) => {
const target = e.target;
if (target === activeSelection) {
hasMoved = true; // 标记发生了移动
const now = Date.now();
// 节流处理,避免过于频繁的更新
if (now - lastUpdateTime < UPDATE_THRESHOLD) {
return;
}
if (isUpdating) {
return;
}
isUpdating = true;
lastUpdateTime = now;
// 使用 requestAnimationFrame 优化渲染
requestAnimationFrame(() => {
try {
// 计算移动距离
const deltaX = target.left - initialLeft;
const deltaY = target.top - initialTop;
// 创建更新遮罩位置的命令
const command = new UpdateGroupMaskPositionCommand({
canvas: this.canvas,
layerManager: this,
layers: this.layers,
layerId: layer.id,
deltaX: deltaX,
deltaY: deltaY,
maskInitialLeft: maskInitialLeft,
maskInitialTop: maskInitialTop,
isExecuteRealtime: true,
});
// 执行实时更新
command.executeRealtime();
} finally {
isUpdating = false;
}
});
}
};
// 修改事件处理函数 - 使用 object:modified 替代 object:moved
const handleModified = (e) => {
const target = e.target;
if (target === activeSelection && hasMoved) {
console.log("✅ 组选择对象移动完成");
// 计算最终移动距离
const deltaX = target.left - initialLeft;
const deltaY = target.top - initialTop;
// 如果有实际移动,创建可撤销的命令
if (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1) {
const command = new UpdateGroupMaskPositionCommand({
canvas: this.canvas,
layers: this.layers,
layerManager: this,
layerId: layer.id,
deltaX: deltaX,
deltaY: deltaY,
maskInitialLeft: maskInitialLeft,
maskInitialTop: maskInitialTop,
activeSelection,
});
// 执行可撤销的命令
if (this.commandManager) {
this.commandManager.execute(command);
} else {
command.execute();
}
}
hasMoved = false; // 重置移动状态
}
};
// 鼠标抬起事件处理 - 备用方案
const handleMouseUp = (e) => {
if (hasMoved && this.canvas.getActiveObject() === activeSelection) {
console.log("🖱️ 鼠标抬起 - 备用移动完成处理");
handleModified(e);
}
};
// 清理事件监听器的函数
const cleanup = () => {
this.canvas.off("object:moving", handleMoving);
this.canvas.off("object:modified", handleModified);
this.canvas.off("mouse:down", handleMovingStart);
this.canvas.off("mouse:up", handleMouseUp);
this.canvas.off("selection:cleared", cleanup);
this.canvas.off("selection:updated", cleanup);
console.log("🧹 清理组遮罩移动同步事件监听器");
};
// 绑定事件监听器
this.canvas.on("mouse:down", handleMovingStart);
this.canvas.on("object:moving", handleMoving);
this.canvas.on("object:modified", handleModified); // 使用 modified 替代 moved
this.canvas.on("mouse:up", handleMouseUp); // 备用方案
// 当选择被清除或更新时清理事件监听器
this.canvas.on("selection:cleared", cleanup);
this.canvas.on("selection:updated", cleanup);
console.log("🎨 已设置组遮罩移动同步 - 使用 object:modified 事件");
}
}

View File

@@ -141,9 +141,7 @@ export class LiquifyReferenceManager {
const snapshot = this.stateSnapshots.get(snapshotId);
if (!fabricObject || !snapshot) {
throw new Error(
`无法恢复快照: 对象或快照不存在 (${refId}, ${snapshotId})`
);
throw new Error(`无法恢复快照: 对象或快照不存在 (${refId}, ${snapshotId})`);
}
// 恢复图像数据
@@ -175,10 +173,7 @@ export class LiquifyReferenceManager {
for (const update of updates) {
try {
const result = await this.updateObjectImageData(
update.refId,
update.imageData
);
const result = await this.updateObjectImageData(update.refId, update.imageData);
results.push({ refId: update.refId, success: true, result });
} catch (error) {
console.error(`批量更新失败 ${update.refId}:`, error);
@@ -253,19 +248,10 @@ export class LiquifyReferenceManager {
const listeners = {};
// 备份常见的事件监听器
const eventTypes = [
"mousedown",
"mouseup",
"mousemove",
"mouseout",
"mouseover",
];
const eventTypes = ["mousedown", "mouseup", "mousemove", "mouseout", "mouseover"];
eventTypes.forEach((eventType) => {
if (
fabricObject.__eventListeners &&
fabricObject.__eventListeners[eventType]
) {
if (fabricObject.__eventListeners && fabricObject.__eventListeners[eventType]) {
listeners[eventType] = [...fabricObject.__eventListeners[eventType]];
}
});
@@ -378,11 +364,7 @@ export class LiquifyReferenceManager {
*/
_captureImageData(fabricObject) {
try {
if (
fabricObject._element &&
fabricObject._element.width &&
fabricObject._element.height
) {
if (fabricObject._element && fabricObject._element.width && fabricObject._element.height) {
const canvas = document.createElement("canvas");
canvas.width = fabricObject._element.width;
canvas.height = fabricObject._element.height;

View File

@@ -55,14 +55,10 @@ export class RedGreenModeManager {
if (typeof options.normalLayerOpacity === "number") {
if (options.normalLayerOpacity > 1) {
// 如果大于1认为是百分比值(0-100)
this.normalLayerOpacity =
Math.max(0, Math.min(100, options.normalLayerOpacity)) / 100;
this.normalLayerOpacity = Math.max(0, Math.min(100, options.normalLayerOpacity)) / 100;
} else {
// 如果小于等于1认为是小数值(0-1)
this.normalLayerOpacity = Math.max(
0,
Math.min(1, options.normalLayerOpacity)
);
this.normalLayerOpacity = Math.max(0, Math.min(1, options.normalLayerOpacity));
}
}
@@ -97,10 +93,7 @@ export class RedGreenModeManager {
this.registerRedGreenMouseUpEvent();
// 启用图层管理器的红绿图模式
if (
this.layerManager &&
typeof this.layerManager.enableRedGreenMode === "function"
) {
if (this.layerManager && typeof this.layerManager.enableRedGreenMode === "function") {
this.layerManager.enableRedGreenMode();
}
@@ -177,26 +170,24 @@ export class RedGreenModeManager {
normalizedOpacity = Math.max(0, Math.min(1, opacity));
}
// 创建透明度更新命令
const opacityCommand = new UpdateNormalLayerOpacityCommand({
canvas: this.canvas,
layerManager: this.layerManager,
opacity: normalizedOpacity,
});
// // 创建透明度更新命令
// const opacityCommand = new UpdateNormalLayerOpacityCommand({
// canvas: this.canvas,
// layerManager: this.layerManager,
// opacity: normalizedOpacity,
// });
// 执行命令
if (this.commandManager) {
this.commandManager.execute(opacityCommand);
} else {
opacityCommand.execute();
}
// // 执行命令
// if (this.commandManager) {
// this.commandManager.execute(opacityCommand);
// } else {
// opacityCommand.execute();
// }
// 更新内部状态
this.normalLayerOpacity = normalizedOpacity;
console.log(
`普通图层透明度已更新为: ${Math.round(normalizedOpacity * 100)}%`
);
console.log(`普通图层透明度已更新为: ${Math.round(normalizedOpacity * 100)}%`);
return true;
} catch (error) {
console.error("更新普通图层透明度失败:", error);
@@ -280,9 +271,7 @@ export class RedGreenModeManager {
const layers = this.layerManager.layers.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
const normalLayers = layers.filter((layer) => !layer.isBackground && !layer.isFixed);
return {
backgroundLayer:
@@ -318,10 +307,7 @@ export class RedGreenModeManager {
cleanup() {
try {
// 禁用图层管理器的红绿图模式
if (
this.layerManager &&
typeof this.layerManager.disableRedGreenMode === "function"
) {
if (this.layerManager && typeof this.layerManager.disableRedGreenMode === "function") {
this.layerManager.disableRedGreenMode();
}

View File

@@ -11,7 +11,8 @@ export class ThumbnailManager {
this.layers = options.layers || []; // 图层管理器
this.layerThumbSize = options.layerThumbSize || { width: 48, height: 48 };
this.layerThumbnails = new Map(); // 图层缩略图缓存
// this.layerThumbnails = new Map(); // 图层缩略图缓存 - 改成使用图层对象的thumbnailUrl属性
// 使用图层对象的thumbnailUrl属性来存储缩略图URL
this.defaultThumbnail =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // 1x1 透明图
}
@@ -23,17 +24,24 @@ export class ThumbnailManager {
async generateLayerThumbnail(layerId) {
const fabricObjects = this._collectLayersAndObjects(layerId);
if (!fabricObjects || fabricObjects.length === 0) {
console.warn("⚠️ 无法生成缩略图:没有可栅格化的对象 返回空缩略图");
// 如果没有对象,返回默认缩略图
const { layer } = findLayerRecursively(this.layers.value, layerId);
if (layer) {
layer.thumbnailUrl = this.defaultThumbnail; // 更新图层对象的缩略图
}
return this.defaultThumbnail;
}
// 延迟执行避免阻塞UI
fabricObjects.length > 0 &&
requestIdleCallback(() => {
setTimeout(async () => {
const base64 = await this._generateLayerThumbnailNow(fabricObjects);
this.layerThumbnails.set(layerId, base64);
// this.layerThumbnails.set(layerId, base64);
try {
const { layer, parent } = findLayerRecursively(
this.layers.value,
layerId
);
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
if (layer) {
layer.thumbnailUrl = base64; // 更新图层对象的缩略图
}
@@ -201,7 +209,7 @@ export class ThumbnailManager {
*/
getLayerThumbnail(layerId) {
if (!layerId) return null;
return this.layerThumbnails.get(layerId) || null;
// return this.layerThumbnails.get(layerId) || null;
}
/**
@@ -210,7 +218,7 @@ export class ThumbnailManager {
*/
clearLayerThumbnail(layerId) {
if (layerId && this.layerThumbnails.has(layerId)) {
this.layerThumbnails.delete(layerId);
// this.layerThumbnails.delete(layerId);
}
}
@@ -218,7 +226,7 @@ export class ThumbnailManager {
* 清除所有缩略图
*/
clearAllThumbnails() {
this.layerThumbnails.clear();
// this.layerThumbnails.clear();
}
/**

View File

@@ -258,10 +258,7 @@ export class ToolManager {
const shiftKey = event.shiftKey;
// 当处于输入状态时不触发快捷键
if (
event.target.tagName === "INPUT" ||
event.target.tagName === "TEXTAREA"
) {
if (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA") {
return;
}
@@ -286,10 +283,7 @@ export class ToolManager {
}
// 在红绿图模式下检查工具可用性
if (
this.isRedGreenMode &&
!this.isToolAvailableInRedGreenMode(toolId)
) {
if (this.isRedGreenMode && !this.isToolAvailableInRedGreenMode(toolId)) {
continue;
}
@@ -564,9 +558,7 @@ export class ToolManager {
// 通知选区管理器切换到矩形套索工具
if (this.canvasManager && this.canvasManager.selectionManager) {
this.canvasManager.selectionManager.setCurrentTool(
OperationType.LASSO_RECTANGLE
);
this.canvasManager.selectionManager.setCurrentTool(OperationType.LASSO_RECTANGLE);
}
}
@@ -581,9 +573,7 @@ export class ToolManager {
// 通知选区管理器切换到椭圆套索工具
if (this.canvasManager && this.canvasManager.selectionManager) {
this.canvasManager.selectionManager.setCurrentTool(
OperationType.LASSO_ELLIPSE
);
this.canvasManager.selectionManager.setCurrentTool(OperationType.LASSO_ELLIPSE);
}
}
@@ -598,9 +588,7 @@ export class ToolManager {
// 通知选区管理器切换到椭圆套索工具
if (this.canvasManager && this.canvasManager.selectionManager) {
this.canvasManager.selectionManager.setCurrentTool(
OperationType.AREA_CUSTOM
);
this.canvasManager.selectionManager.setCurrentTool(OperationType.AREA_CUSTOM);
}
}
@@ -618,9 +606,7 @@ export class ToolManager {
console.log("矩形选区工具已激活");
if (this.canvasManager && this.canvasManager.selectionManager) {
this.canvasManager.selectionManager.setCurrentTool(
OperationType.AREA_RECTANGLE
);
this.canvasManager.selectionManager.setCurrentTool(OperationType.AREA_RECTANGLE);
}
}
@@ -677,10 +663,7 @@ export class ToolManager {
if (layer) {
if (layer.isBackground || layer.type === "background") {
panelDetail.targetObject = layer.fabricObject;
} else if (
layer.fabricObjects &&
layer.fabricObjects.length > 0
) {
} else if (layer.fabricObjects && layer.fabricObjects.length > 0) {
panelDetail.targetObject = layer.fabricObjects[0];
}
@@ -906,9 +889,7 @@ export class ToolManager {
});
// 准备液化操作,获取原始图像数据
const prepareResult = await liquifyManager.prepareForLiquify(
targetObject
);
const prepareResult = await liquifyManager.prepareForLiquify(targetObject);
// 创建和初始化命令
const initCommand = new InitLiquifyToolCommand({
@@ -1110,9 +1091,7 @@ export class ToolManager {
const target = e.target;
if (
target &&
(target.type === "text" ||
target.type === "i-text" ||
target.type === "textbox")
(target.type === "text" || target.type === "i-text" || target.type === "textbox")
) {
// 获取对应的图层
const layer = this.layerManager.getLayerById(target.layerId);
@@ -1380,11 +1359,7 @@ export class ToolManager {
}
// 从画布笔刷获取
if (
this.canvas &&
this.canvas.freeDrawingBrush &&
this.canvas.freeDrawingBrush.width
) {
if (this.canvas && this.canvas.freeDrawingBrush && this.canvas.freeDrawingBrush.width) {
return this.canvas.freeDrawingBrush.width;
}
@@ -1409,11 +1384,7 @@ export class ToolManager {
}
// 从画布笔刷获取
if (
this.canvas &&
this.canvas.freeDrawingBrush &&
this.canvas.freeDrawingBrush.color
) {
if (this.canvas && this.canvas.freeDrawingBrush && this.canvas.freeDrawingBrush.color) {
return this.canvas.freeDrawingBrush.color;
}

View File

@@ -87,8 +87,7 @@ export class AnimationManager {
// 计算过渡动画持续时间 - 根据当前值到目标值的距离比例
const progressRatio =
Math.abs(targetZoom - currentZoomValue) /
Math.abs(targetZoom - currentZoom);
Math.abs(targetZoom - currentZoomValue) / Math.abs(targetZoom - currentZoom);
const duration = options.duration || 0.3 * progressRatio;
// 计算缩放后目标位置需要的修正,保持缩放点不变
@@ -425,17 +424,17 @@ export class AnimationManager {
const now = Date.now();
// Mac设备的轻量防抖检查 - 进一步减少冷却时间,确保响应性
if (
this._isMac &&
now - this._lastMacAnimationTime < this._macAnimationCooldown
) {
if (this._isMac && now - this._lastMacAnimationTime < this._macAnimationCooldown) {
// 如果距离上次动画时间太短,只延迟很短时间,不阻塞太久
if (this._wheelAccumulationTimeout) {
clearTimeout(this._wheelAccumulationTimeout);
}
this._wheelAccumulationTimeout = setTimeout(() => {
this._processAccumulatedWheel(lastOpt);
}, Math.min(this._macAnimationCooldown, 3)); // 最多延迟3ms
this._wheelAccumulationTimeout = setTimeout(
() => {
this._processAccumulatedWheel(lastOpt);
},
Math.min(this._macAnimationCooldown, 3)
); // 最多延迟3ms
return;
}
@@ -513,28 +512,16 @@ export class AnimationManager {
// Mac设备使用平衡的动画时间控制
if (speedFactor > 2) {
// 快速操作:快速但平滑
duration = Math.min(
0.18,
Math.max(0.08, (zoomRatio * 0.3) / Math.sqrt(speedFactor))
);
duration = Math.min(0.18, Math.max(0.08, (zoomRatio * 0.3) / Math.sqrt(speedFactor)));
} else if (speedFactor > 1.2) {
// 中等速度:标准响应
duration = Math.min(
0.25,
Math.max(0.1, (zoomRatio * 0.4) / Math.sqrt(speedFactor))
);
duration = Math.min(0.25, Math.max(0.1, (zoomRatio * 0.4) / Math.sqrt(speedFactor)));
} else {
// 慢速精确操作:确保平滑
duration = Math.min(
0.3,
Math.max(0.12, (zoomRatio * 0.5) / Math.sqrt(speedFactor))
);
duration = Math.min(0.3, Math.max(0.12, (zoomRatio * 0.5) / Math.sqrt(speedFactor)));
}
} else {
duration = Math.min(
0.5,
Math.max(0.15, (zoomRatio * 0.8) / Math.sqrt(speedFactor))
);
duration = Math.min(0.5, Math.max(0.15, (zoomRatio * 0.8) / Math.sqrt(speedFactor)));
}
// 根据滚动速度选择不同的缓动效果

View File

@@ -125,9 +125,7 @@ export class BaseBrush {
_createRGBAColor(color, opacity) {
// 如果已经是rgba颜色先提取RGB部分
if (color.startsWith("rgba")) {
const rgbaMatch = color.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/
);
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
if (rgbaMatch) {
const [, r, g, b] = rgbaMatch;
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
@@ -258,10 +256,7 @@ export class BaseBrush {
// 过滤掉同名属性(子类优先)
const basePropsFiltered = baseProperties.filter(
(baseProp) =>
!specificProperties.some(
(specificProp) => specificProp.id === baseProp.id
)
(baseProp) => !specificProperties.some((specificProp) => specificProp.id === baseProp.id)
);
return [...basePropsFiltered, ...specificProperties];
@@ -334,10 +329,7 @@ export class BaseBrush {
* @returns {Array} 选项数组
*/
getDynamicOptions(property, currentValues) {
if (
property.dynamicOptions &&
typeof property.dynamicOptions === "function"
) {
if (property.dynamicOptions && typeof property.dynamicOptions === "function") {
return property.dynamicOptions(currentValues);
}
return property.options || [];

View File

@@ -206,9 +206,7 @@ export class TexturePresetManager {
* @returns {Object|null} 材质对象
*/
getTextureById(textureId) {
return (
this.getAllTextures().find((texture) => texture.id === textureId) || null
);
return this.getAllTextures().find((texture) => texture.id === textureId) || null;
}
/**
@@ -217,9 +215,7 @@ export class TexturePresetManager {
* @returns {Array} 材质数组
*/
getTexturesByCategory(category) {
return this.getAllTextures().filter(
(texture) => texture.category === category
);
return this.getAllTextures().filter((texture) => texture.category === category);
}
/**
@@ -241,8 +237,7 @@ export class TexturePresetManager {
*/
addCustomTexture(textureData) {
const textureId =
textureData.id ||
`custom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
textureData.id || `custom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const texture = {
id: textureId,
@@ -286,9 +281,7 @@ export class TexturePresetManager {
* @returns {Boolean} 是否删除成功
*/
removeCustomTexture(textureId) {
const index = this.customTextures.findIndex(
(texture) => texture.id === textureId
);
const index = this.customTextures.findIndex((texture) => texture.id === textureId);
if (index === -1) {
return false;
}
@@ -430,10 +423,7 @@ export class TexturePresetManager {
file: null,
}));
localStorage.setItem(
"canvasEditor_customTextures",
JSON.stringify(customTexturesData)
);
localStorage.setItem("canvasEditor_customTextures", JSON.stringify(customTexturesData));
} catch (error) {
console.error("保存自定义材质失败:", error);
}
@@ -472,9 +462,7 @@ export class TexturePresetManager {
* @returns {String} 预设ID
*/
createTexturePreset(name, settings) {
const presetId = `preset_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const presetId = `preset_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const preset = {
id: presetId,

View File

@@ -17,6 +17,7 @@ import { MarkerBrush } from "./types/MarkerBrush";
import { CustomPenBrush } from "./types/CustomPenBrush";
import { RibbonBrush } from "./types/RibbonBrush";
import { ShadedBrush } from "./types/ShadedBrush";
import { EraserStateManager } from "../EraserStateManager";
// import { SketchyBrush } from "./types/SketchyBrush";
// import { SpraypaintBrush } from "./types/SpraypaintBrush";
@@ -379,27 +380,23 @@ export class BrushManager {
}
// 创建新笔刷实例
try {
const brushInstance = brushRegistry.createBrushInstance(
brushId,
this.canvas,
{
color: brushId === "eraser" ? this.brushStore.state.color : undefined,
width: this.brushStore.state.size,
opacity: this.brushStore.state.opacity,
const brushInstance = brushRegistry.createBrushInstance(brushId, this.canvas, {
color: brushId === "eraser" ? this.brushStore.state.color : undefined,
width: this.brushStore.state.size,
opacity: this.brushStore.state.opacity,
// 阴影相关配置
shadowEnabled: this.brushStore.state.shadowEnabled,
shadowColor: this.brushStore.state.shadowColor,
shadowWidth: this.brushStore.state.shadowWidth,
shadowOffsetX: this.brushStore.state.shadowOffsetX,
shadowOffsetY: this.brushStore.state.shadowOffsetY,
// 阴影相关配置
shadowEnabled: this.brushStore.state.shadowEnabled,
shadowColor: this.brushStore.state.shadowColor,
shadowWidth: this.brushStore.state.shadowWidth,
shadowOffsetX: this.brushStore.state.shadowOffsetX,
shadowOffsetY: this.brushStore.state.shadowOffsetY,
// 材质笔刷特有配置
textureEnabled: this.brushStore.state.textureEnabled,
texturePath: this.brushStore.state.texturePath,
textureScale: this.brushStore.state.textureScale,
}
);
// 材质笔刷特有配置
textureEnabled: this.brushStore.state.textureEnabled,
texturePath: this.brushStore.state.texturePath,
textureScale: this.brushStore.state.textureScale,
});
if (brushInstance) {
// 创建笔刷
@@ -648,48 +645,7 @@ export class BrushManager {
// 初始化橡皮擦状态管理器
if (this.canvas && this.layerManager) {
this.eraserStateManager = new EraserStateManager(
this.canvas,
this.layerManager
);
}
}
/**
* 创建橡皮擦
* @param {Object} options 橡皮擦选项
*/
createEraser(options = {}) {
if (!this.canvas) {
console.error("画布未初始化");
return null;
}
try {
// 直接使用 fabric-with-erasing 库提供的 EraserBrush
this.brush = new fabric.EraserBrush(this.canvas);
// 应用配置
this.configure(this.brush, {
width: this.brushSize.value,
color: this.brushColor.value,
opacity: this.brushOpacity.value,
inverted: options.inverted || false,
...options,
});
// 设置画布为绘图模式
this.canvas.isDrawingMode = true;
this.canvas.freeDrawingBrush = this.brush;
// 绑定橡皮擦事件处理器
// this._bindEraserEvents();
console.log("橡皮擦创建成功");
return this.brush;
} catch (error) {
console.error("创建橡皮擦失败:", error);
return null;
this.eraserStateManager = new EraserStateManager(this.canvas, this.layerManager);
}
}
@@ -866,6 +822,44 @@ export class BrushManager {
}
}
// /**
// * 创建橡皮擦
// * @param {Object} options 橡皮擦选项
// */
// createEraser(options = {}) {
// if (!this.canvas) {
// console.error("画布未初始化");
// return null;
// }
// try {
// // 直接使用 fabric-with-erasing 库提供的 EraserBrush
// this.brush = new fabric.EraserBrush(this.canvas);
// // 应用配置
// this.configure(this.brush, {
// width: this.brushSize.value,
// color: this.brushColor.value,
// opacity: this.brushOpacity.value,
// inverted: options.inverted || false,
// ...options,
// });
// // 设置画布为绘图模式
// this.canvas.isDrawingMode = true;
// this.canvas.freeDrawingBrush = this.brush;
// // 绑定橡皮擦事件处理器
// // this._bindEraserEvents();
// console.log("橡皮擦创建成功");
// return this.brush;
// } catch (error) {
// console.error("创建橡皮擦失败:", error);
// return null;
// }
// }
/**
* 创建橡皮擦
* @returns {Object} 橡皮擦笔刷
@@ -891,12 +885,7 @@ export class BrushManager {
const imageData = ctx.getImageData(pointer.x, pointer.y, 1, 1).data;
// 将RGB转换为十六进制颜色
const color = `#${(
(1 << 24) +
(imageData[0] << 16) +
(imageData[1] << 8) +
imageData[2]
)
const color = `#${((1 << 24) + (imageData[0] << 16) + (imageData[1] << 8) + imageData[2])
.toString(16)
.slice(1)}`;

View File

@@ -97,9 +97,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
}
if (color.indexOf("rgb") === -1) {
// convert named colors
var tempElem = document.body.appendChild(
document.createElement("fictum")
); // intentionally use unknown tag to lower chances of css rule override with !important
var tempElem = document.body.appendChild(document.createElement("fictum")); // intentionally use unknown tag to lower chances of css rule override with !important
var flag = "rgb(1, 2, 3)"; // this flag tested on chrome 59, ff 53, ie9, ie10, ie11, edge 14
tempElem.style.color = flag;
if (tempElem.style.color !== flag) {
@@ -116,7 +114,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
if (color.indexOf("rgba") === -1) {
color += ",1"; // convert 'rgb(R,G,B)' to 'rgb(R,G,B)A' which looks awful but will pass the regxep below
}
return color.match(/[\.\d]+/g).map(function (a) {
return color.match(/[.\d]+/g).map(function (a) {
return +a;
});
}
@@ -269,13 +267,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
draw: function () {
var ctx = this.ctx;
ctx.save();
this.line(
ctx,
this._lastPoint,
this._point,
this.color,
this._currentLineWidth
);
this.line(ctx, this._lastPoint, this._point, this.color, this._currentLineWidth);
ctx.restore();
},
@@ -319,10 +311,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this.canvas.contextTop.globalAlpha = this.opacity;
this._size = this.width / 2 + this._baseWidth;
this._drawn = false;
@@ -330,10 +320,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this.update(pointer);
this.draw(this.canvas.contextTop);
},
@@ -358,9 +346,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
update: function (p) {
this.set(p);
this._latestStrokeLength = this._point
.subtract(this._latest)
.distanceFrom({ x: 0, y: 0 });
this._latestStrokeLength = this._point.subtract(this._latest).distanceFrom({ x: 0, y: 0 });
},
draw: function (ctx) {
@@ -372,12 +358,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
v.normalize(s);
dotSize =
this._sep *
fabric.util.clamp(
(this._inkAmount / this._latestStrokeLength) * 3,
1,
0.5
);
this._sep * fabric.util.clamp((this._inkAmount / this._latestStrokeLength) * 3, 1, 0.5);
dotNum = Math.ceil(this._size * this._sep);
range = this._size / 2;
@@ -428,10 +409,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseDown: function (pointer) {
// 添加坐标转换处理画布缩放和偏移
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points = [pointer];
this._count = 0;
@@ -440,15 +419,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
color = fabric.util.colorValues(this.color);
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
0.1 * this.opacity +
")";
"rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + 0.1 * this.opacity + ")";
ctx.lineWidth = this.width;
this._points.push(pointer);
@@ -456,10 +427,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseMove: function (pointer) {
// 添加坐标转换处理画布缩放和偏移
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points.push(pointer);
@@ -528,10 +497,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
_render: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
var len,
i,
point = this.setPointer(pointer),
@@ -579,21 +546,11 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
for (i = 0; i < num; i++) {
r = fabric.util.getRandom(range, 1);
c = fabric.util.getRandom(Math.PI * 2);
point = new fabric.Point(
pointer.x + r * Math.sin(c),
pointer.y + r * Math.cos(c)
);
point = new fabric.Point(pointer.x + r * Math.sin(c), pointer.y + r * Math.cos(c));
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(
point.x,
point.y,
fabric.util.getRandom(maxSize) / 2,
0,
Math.PI * 2,
false
);
ctx.arc(point.x, point.y, fabric.util.getRandom(maxSize) / 2, 0, Math.PI * 2, false);
ctx.fill();
}
ctx.restore();
@@ -609,10 +566,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
_resetTip: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
var len,
i,
point = this.setPointer(pointer);
@@ -655,10 +610,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points = [pointer];
this._count = 0;
@@ -667,23 +620,13 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
//ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
0.05 * this.opacity +
")";
"rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + 0.05 * this.opacity + ")";
ctx.lineWidth = this.width;
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points.push(pointer);
var i,
@@ -772,10 +715,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
this._lastPoint.x - lineWidthDiff + num,
this._lastPoint.y + lineWidthDiff - num
);
ctx.lineTo(
pointer.x - lineWidthDiff + num,
pointer.y + lineWidthDiff - num
);
ctx.lineTo(pointer.x - lineWidthDiff + num, pointer.y + lineWidthDiff - num);
ctx.stroke();
}
@@ -783,10 +723,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
this.canvas.contextTop.strokeStyle = this.color;
this.canvas.contextTop.lineWidth = this._lineWidth;
@@ -795,10 +733,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseMove: function (pointer) {
if (this.canvas._isCurrentlyDrawing) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._render(pointer);
}
},
@@ -852,10 +788,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
this._lastPoint.x + lineWidthDiff - num,
this._lastPoint.y + lineWidthDiff - num
);
ctx.lineTo(
pointer.x + lineWidthDiff - num,
pointer.y + lineWidthDiff - num
);
ctx.lineTo(pointer.x + lineWidthDiff - num, pointer.y + lineWidthDiff - num);
ctx.stroke();
}
@@ -863,10 +796,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
this.canvas.contextTop.strokeStyle = this.color;
this.canvas.contextTop.lineWidth = this._lineWidth;
@@ -875,10 +806,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseMove: function (pointer) {
if (this.canvas._isCurrentlyDrawing) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._render(pointer);
}
},
@@ -923,10 +852,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points = [];
this._points.push(["M", pointer.x, pointer.y]);
@@ -938,10 +865,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseMove: function (pointer) {
if (this.canvas._isCurrentlyDrawing) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._render(pointer);
}
},
@@ -1010,10 +935,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseDown: function (pointer) {
// 添加坐标转换处理画布缩放和偏移
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
this.canvas.contextTop.strokeStyle = this.color;
@@ -1024,10 +947,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseMove: function (pointer) {
if (this.canvas._isCurrentlyDrawing) {
// 添加坐标转换处理画布缩放和偏移
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._render(pointer);
}
@@ -1093,10 +1014,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
this.canvas.contextTop.lineWidth = this._lineWidth;
this._size = this.width + this._baseWidth;
@@ -1104,10 +1023,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseMove: function (pointer) {
if (this.canvas._isCurrentlyDrawing) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._render(pointer);
}
},
@@ -1158,12 +1075,10 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
ctx.beginPath();
ctx.moveTo(painters[i].dx, painters[i].dy);
painters[i].dx -= painters[i].ax =
(painters[i].ax +
(painters[i].dx - this._lastPoint.x) * painters[i].div) *
(painters[i].ax + (painters[i].dx - this._lastPoint.x) * painters[i].div) *
painters[i].ease;
painters[i].dy -= painters[i].ay =
(painters[i].ay +
(painters[i].dy - this._lastPoint.y) * painters[i].div) *
(painters[i].ay + (painters[i].dy - this._lastPoint.y) * painters[i].div) *
painters[i].ease;
ctx.lineTo(painters[i].dx, painters[i].dy);
ctx.stroke();
@@ -1185,26 +1100,16 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
ease: Math.random() * 0.2 + 0.6,
});
}
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
//ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
0.05 * this.opacity +
")";
"rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + 0.05 * this.opacity + ")";
ctx.lineWidth = this.width;
for (var i = 0; i < this._nrPainters; i++) {
for (let i = 0; i < this._nrPainters; i++) {
this._painters[i].dx = pointer.x;
this._painters[i].dy = pointer.y;
}
@@ -1216,10 +1121,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
},
@@ -1254,34 +1157,22 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points = [pointer];
var ctx = this.canvas.contextTop,
color = fabric.util.colorValues(this.color);
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
this.opacity +
")";
"rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + this.opacity + ")";
ctx.lineWidth = this.width;
ctx.lineJoin = ctx.lineCap = "round";
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points.push(pointer);
var ctx = this.canvas.contextTop,
@@ -1346,10 +1237,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
this.canvas.contextTop.globalAlpha = this.opacity;
// 坐标转换
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points = [pointer];
this._count = 0;
@@ -1361,23 +1250,13 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
ctx.lineWidth = this.width;
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
0.3 * this.opacity +
")";
"rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + 0.3 * this.opacity + ")";
},
onMouseMove: function (pointer) {
// 坐标转换
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points.push(pointer);
@@ -1396,15 +1275,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
// 增加透明度设置
var color = fabric.util.colorValues(this.color);
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
0.2 * this.opacity +
")";
"rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + 0.2 * this.opacity + ")";
// 修改循环逻辑,确保在有点时能画出效果
if (this._count > 0) {
@@ -1416,10 +1287,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
if (d < 4000 && Math.random() > d / 2000) {
ctx.beginPath();
ctx.moveTo(
points[this._count].x + dx * factor,
points[this._count].y + dy * factor
);
ctx.moveTo(points[this._count].x + dx * factor, points[this._count].y + dy * factor);
ctx.lineTo(points[i].x - dx * factor, points[i].y - dy * factor);
ctx.stroke();
this._drawn = true;
@@ -1494,10 +1362,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
onMouseDown: function (pointer) {
this.canvas.contextTop.globalAlpha = this.opacity;
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._point = new fabric.Point(pointer.x, pointer.y);
this._lastPoint = this._point;
@@ -1510,10 +1376,8 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = this._point;
this._point = new fabric.Point(pointer.x, pointer.y);
},
@@ -1542,13 +1406,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
x = self._lastPoint.x + Math.sin(angle) - self.size / 2;
y = self._lastPoint.y + Math.cos(angle) - self.size / 2;
self.canvas.contextTop.drawImage(
self.brush._element,
x,
y,
self.size,
self.size
);
self.canvas.contextTop.drawImage(self.brush._element, x, y, self.size, self.size);
if (self.canvas._isCurrentlyDrawing) {
setTimeout(draw, self._interval);
@@ -1594,43 +1452,22 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
var ctx = this.canvas.contextTop,
color = fabric.util.colorValues(this.color),
bgColor = fabric.util.colorValues(this.bgColor);
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._lastPoint = pointer;
this._drawn = false;
//ctx.globalCompositeOperation = 'source-over';
this.canvas.contextTop.globalAlpha = this.opacity;
ctx.fillStyle =
"rgba(" +
bgColor[0] +
"," +
bgColor[1] +
"," +
bgColor[2] +
"," +
bgColor[3] +
")";
ctx.strokeStyle =
"rgba(" +
color[0] +
"," +
color[1] +
"," +
color[2] +
"," +
color[3] +
")";
"rgba(" + bgColor[0] + "," + bgColor[1] + "," + bgColor[2] + "," + bgColor[3] + ")";
ctx.strokeStyle = "rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + color[3] + ")";
ctx.lineWidth = this.width;
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
var ctx = this.canvas.contextTop,
dx = pointer.x - this._lastPoint.x,
dy = pointer.y - this._lastPoint.y,
@@ -1683,20 +1520,16 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
},
onMouseDown: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points = [pointer];
this._count = 0;
this._colorValues = fabric.util.colorValues(this.color);
},
onMouseMove: function (pointer) {
pointer.x =
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y =
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
pointer.x = pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
pointer.y = pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
this._points.push(pointer);
var ctx = this.canvas.contextTop,

View File

@@ -350,9 +350,7 @@ export class RainbowBrush extends BaseBrush {
];
// 移除基础属性中的颜色选择器,因为彩虹笔不需要颜色选择
const filteredBaseProps = baseProperties.filter(
(prop) => prop.id !== "color"
);
const filteredBaseProps = baseProperties.filter((prop) => prop.id !== "color");
// 合并并返回所有属性
return [...filteredBaseProps, ...rainbowProperties];
@@ -432,11 +430,7 @@ export class CustomBrushExample extends BaseBrush {
this.dashPattern = options.dashPattern || [0, 0]; // 实线
this.noiseAmount = options.noiseAmount || 0;
this.gradientEnabled = options.gradientEnabled || false;
this.gradientColors = options.gradientColors || [
"#ff0000",
"#ffff00",
"#00ff00",
];
this.gradientColors = options.gradientColors || ["#ff0000", "#ffff00", "#00ff00"];
this.angle = options.angle || 0;
// 材质相关属性
@@ -885,8 +879,7 @@ export class CustomBrushExample extends BaseBrush {
description: "选择图案类型",
category: "特效设置",
order: 130,
visibleWhen: (values) =>
values.effectType === "pattern" || values.patternType === "dashed",
visibleWhen: (values) => values.effectType === "pattern" || values.patternType === "dashed",
},
// 虚线特有属性
@@ -1184,7 +1177,7 @@ export class CustomBrushExample extends BaseBrush {
return true;
case "dashPattern1":
case "dashPattern2":
case "dashPattern2": {
// 更新虚线模式
const idx = propId === "dashPattern1" ? 0 : 1;
this.dashPattern[idx] = value;
@@ -1192,6 +1185,7 @@ export class CustomBrushExample extends BaseBrush {
this.brush.strokeDashArray = this.dashPattern;
}
return true;
}
case "noiseAmount":
this.noiseAmount = value;
@@ -1206,13 +1200,14 @@ export class CustomBrushExample extends BaseBrush {
case "gradientColor1":
case "gradientColor2":
case "gradientColor3":
case "gradientColor3": {
const colorIdx = parseInt(propId.slice(-1)) - 1;
this.gradientColors[colorIdx] = value;
if (this.gradientEnabled && this.brush) {
this._configureGradient(this.brush);
}
return true;
}
case "angle":
this.angle = value;

View File

@@ -24,8 +24,7 @@ export class InkBrush extends BaseBrush {
this._baseWidth = options._baseWidth || 15;
this._inkAmount = options._inkAmount || 7;
this._range = options._range || 10;
this.splashEnabled =
options.splashEnabled !== undefined ? options.splashEnabled : true;
this.splashEnabled = options.splashEnabled !== undefined ? options.splashEnabled : true;
this.splashSize = options.splashSize || 5;
this.splashDistance = options.splashDistance || 30;
}

View File

@@ -26,9 +26,7 @@ export class LongfurBrush extends BaseBrush {
this.furFlowFactor = options.furFlowFactor || 0.5;
this.furCurvature = options.furCurvature || 0.3;
this.randomizeDirection =
options.randomizeDirection !== undefined
? options.randomizeDirection
: true;
options.randomizeDirection !== undefined ? options.randomizeDirection : true;
}
/**

View File

@@ -39,9 +39,7 @@ export class PencilBrush extends BaseBrush {
this.brush = new fabric.PencilBrush(this.canvas);
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
this.brush
);
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(this.brush);
const self = this; // 保存外部this引用
this.brush._finalizeAndAddPath = function () {
@@ -54,10 +52,7 @@ export class PencilBrush extends BaseBrush {
this._points = this.decimatePoints(this._points, this.decimate);
}
console.log(
"PencilBrush: points count =",
this._points ? this._points.length : 0
);
console.log("PencilBrush: points count =", this._points ? this._points.length : 0);
// 检查是否有有效的路径数据
if (!this._points || this._points.length < 2) {
@@ -97,9 +92,7 @@ export class PencilBrush extends BaseBrush {
// 恢复透明度
ctx.globalAlpha = currentAlpha;
} else {
console.warn(
"convertToImg method not found, falling back to original behavior"
);
console.warn("convertToImg method not found, falling back to original behavior");
// 如果没有convertToImg方法回退到原始行为
this.canvas.add(path);
this.canvas.fire("path:created", { path: path });
@@ -135,11 +128,7 @@ export class PencilBrush extends BaseBrush {
const command = pathData[i];
if (command[0] === "M") {
moveCount++;
} else if (
command[0] === "L" ||
command[0] === "Q" ||
command[0] === "C"
) {
} else if (command[0] === "L" || command[0] === "Q" || command[0] === "C") {
hasDrawing = true;
break;
}
@@ -210,9 +199,7 @@ export class PencilBrush extends BaseBrush {
_createRGBAColor(color, opacity) {
// 如果已经是rgba颜色先提取RGB部分
if (color.startsWith("rgba")) {
const rgbaMatch = color.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/
);
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
if (rgbaMatch) {
const [, r, g, b] = rgbaMatch;
return `rgba(${r}, ${g}, ${b}, ${opacity})`;

View File

@@ -75,10 +75,7 @@ export class RibbonBrush extends BaseBrush {
brush.opacity = options.opacity;
// 如果启用渐变,更新渐变的第一个颜色
if (this.gradient && this.gradientColors.length > 0) {
this.gradientColors[0] = this._createRGBAColor(
brush.color,
options.opacity
);
this.gradientColors[0] = this._createRGBAColor(brush.color, options.opacity);
this.updateGradient();
}
brush.canvas.freeDrawingBrush.opacity = options.opacity;

View File

@@ -23,8 +23,7 @@ export class SpraypaintBrush extends BaseBrush {
// 喷漆笔刷特有属性
this.density = options.density || 20;
this.sprayRadius = options.sprayRadius || 10;
this.randomOpacity =
options.randomOpacity !== undefined ? options.randomOpacity : true;
this.randomOpacity = options.randomOpacity !== undefined ? options.randomOpacity : true;
this.dotSize = options.dotSize || 1;
this.dotShape = options.dotShape || "circle";
}

View File

@@ -71,9 +71,7 @@ export class TextureBrush extends BaseBrush {
this.brush = new fabric.PatternBrush(this.canvas);
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
this.brush
);
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(this.brush);
const self = this; // 保存外部this引用
this.brush._finalizeAndAddPath = function () {
@@ -86,10 +84,7 @@ export class TextureBrush extends BaseBrush {
this._points = this.decimatePoints(this._points, this.decimate);
}
console.log(
"TextureBrush: points count =",
this._points ? this._points.length : 0
);
console.log("TextureBrush: points count =", this._points ? this._points.length : 0);
// 检查是否有有效的路径数据
if (!this._points || this._points.length < 2) {
@@ -122,9 +117,7 @@ export class TextureBrush extends BaseBrush {
this.convertToImg();
console.log("TextureBrush: convertToImg called successfully");
} else {
console.warn(
"convertToImg method not found, falling back to original behavior"
);
console.warn("convertToImg method not found, falling back to original behavior");
// 如果没有convertToImg方法回退到原始行为
this.canvas.add(path);
this.canvas.fire("path:created", { path: path });
@@ -254,10 +247,7 @@ export class TextureBrush extends BaseBrush {
this._applyTextureToPatternBrush(img);
resolve(img);
});
} else if (
source instanceof Image ||
source instanceof HTMLCanvasElement
) {
} else if (source instanceof Image || source instanceof HTMLCanvasElement) {
// 如果已经是Image或Canvas对象直接使用
this._applyTextureToPatternBrush(source);
resolve(source);
@@ -425,9 +415,7 @@ export class TextureBrush extends BaseBrush {
if (textures.length === 0) return Promise.resolve();
this.currentTextureIndex =
this.currentTextureIndex === 0
? textures.length - 1
: this.currentTextureIndex - 1;
this.currentTextureIndex === 0 ? textures.length - 1 : this.currentTextureIndex - 1;
const prevTexture = textures[this.currentTextureIndex];
return this.setTextureById(prevTexture.id);
@@ -943,11 +931,7 @@ export class TextureBrush extends BaseBrush {
const command = pathData[i];
if (command[0] === "M") {
moveCount++;
} else if (
command[0] === "L" ||
command[0] === "Q" ||
command[0] === "C"
) {
} else if (command[0] === "L" || command[0] === "Q" || command[0] === "C") {
hasDrawing = true;
break;
}

View File

@@ -25,9 +25,7 @@ export class WritingBrush extends BaseBrush {
this.inkAmount = options.inkAmount || 20;
this.brushTaperFactor = options.brushTaperFactor || 0.6;
this.enableInkDripping =
options.enableInkDripping !== undefined
? options.enableInkDripping
: true;
options.enableInkDripping !== undefined ? options.enableInkDripping : true;
}
/**

View File

@@ -61,14 +61,10 @@ export class PerformanceManager {
: 0;
const avgUndoTime =
this.stats.totalUndos > 0
? this.stats.totalUndoTime / this.stats.totalUndos
: 0;
this.stats.totalUndos > 0 ? this.stats.totalUndoTime / this.stats.totalUndos : 0;
const avgRedoTime =
this.stats.totalRedos > 0
? this.stats.totalRedoTime / this.stats.totalRedos
: 0;
this.stats.totalRedos > 0 ? this.stats.totalRedoTime / this.stats.totalRedos : 0;
return {
overview: {
@@ -79,26 +75,18 @@ export class PerformanceManager {
avgUndoTime: Number(avgUndoTime.toFixed(2)),
avgRedoTime: Number(avgRedoTime.toFixed(2)),
},
commandBreakdown: Array.from(this.stats.commandStats.entries()).map(
([name, stats]) => ({
commandName: name,
executions: stats.executions,
undos: stats.undos,
redos: stats.redos,
avgExecutionTime:
stats.executions > 0
? Number((stats.totalExecutionTime / stats.executions).toFixed(2))
: 0,
avgUndoTime:
stats.undos > 0
? Number((stats.totalUndoTime / stats.undos).toFixed(2))
: 0,
avgRedoTime:
stats.redos > 0
? Number((stats.totalRedoTime / stats.redos).toFixed(2))
: 0,
})
),
commandBreakdown: Array.from(this.stats.commandStats.entries()).map(([name, stats]) => ({
commandName: name,
executions: stats.executions,
undos: stats.undos,
redos: stats.redos,
avgExecutionTime:
stats.executions > 0
? Number((stats.totalExecutionTime / stats.executions).toFixed(2))
: 0,
avgUndoTime: stats.undos > 0 ? Number((stats.totalUndoTime / stats.undos).toFixed(2)) : 0,
avgRedoTime: stats.redos > 0 ? Number((stats.totalRedoTime / stats.redos).toFixed(2)) : 0,
})),
recentOperations: this.stats.recentOperations.slice(-20), // 最近20个操作
};
}
@@ -110,10 +98,8 @@ export class PerformanceManager {
const slowCommands = [];
for (const [name, stats] of this.stats.commandStats.entries()) {
const avgExecTime =
stats.executions > 0 ? stats.totalExecutionTime / stats.executions : 0;
const avgUndoTime =
stats.undos > 0 ? stats.totalUndoTime / stats.undos : 0;
const avgExecTime = stats.executions > 0 ? stats.totalExecutionTime / stats.executions : 0;
const avgUndoTime = stats.undos > 0 ? stats.totalUndoTime / stats.undos : 0;
if (avgExecTime > threshold || avgUndoTime > threshold) {
slowCommands.push({
@@ -128,8 +114,7 @@ export class PerformanceManager {
return slowCommands.sort(
(a, b) =>
Math.max(b.avgExecutionTime, b.avgUndoTime) -
Math.max(a.avgExecutionTime, a.avgUndoTime)
Math.max(b.avgExecutionTime, b.avgUndoTime) - Math.max(a.avgExecutionTime, a.avgUndoTime)
);
}

View File

@@ -68,14 +68,9 @@ export class CanvasEventManager {
// 让动画管理器自行处理冲突,避免过度干预
} else {
// 非 Mac 设备的标准处理
if (
this.animationManager._panAnimation ||
this.animationManager._zoomAnimation
) {
this.animationManager._wasPanning =
!!this.animationManager._panAnimation;
this.animationManager._wasZooming =
!!this.animationManager._zoomAnimation;
if (this.animationManager._panAnimation || this.animationManager._zoomAnimation) {
this.animationManager._wasPanning = !!this.animationManager._panAnimation;
this.animationManager._wasZooming = !!this.animationManager._zoomAnimation;
this.animationManager.smoothStopAnimations({ duration: 0.1 });
}
}
@@ -127,8 +122,7 @@ export class CanvasEventManager {
// Mac 设备优化:触控板滚动通常比鼠标滚轮更敏感
if (this.deviceInfo.isMac) {
const isMacTrackpadScroll =
Math.abs(opt.e.deltaY) < 100 && opt.e.deltaMode === 0;
const isMacTrackpadScroll = Math.abs(opt.e.deltaY) < 100 && opt.e.deltaMode === 0;
if (isMacTrackpadScroll) {
// Mac 触控板滚动更细腻,需要调整滚动因子
scrollFactor *= 0.8; // 降低滚动敏感度
@@ -183,11 +177,7 @@ export class CanvasEventManager {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (
opt.e.altKey ||
opt.e.which === 2 ||
this.editorMode === OperationType.PAN
) {
if (opt.e.altKey || opt.e.which === 2 || this.editorMode === OperationType.PAN) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
@@ -247,10 +237,8 @@ export class CanvasEventManager {
if (opt.e.touches && opt.e.touches.length === 2) {
this.canvas.isDragging = true;
this.canvas.lastPosX =
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
this.canvas.lastPosY =
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
this.canvas.lastPosX = (opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
this.canvas.lastPosY = (opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
// 重置触摸位置历史
this.dragStartTime = Date.now();
@@ -290,10 +278,8 @@ export class CanvasEventManager {
if (!this.canvas.isDragging) return;
if (opt.e.touches && opt.e.touches.length === 2) {
const currentX =
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
const currentY =
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
const currentX = (opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
const currentY = (opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
const vpt = this.canvas.viewportTransform;
vpt[4] += currentX - this.canvas.lastPosX;
@@ -321,8 +307,7 @@ export class CanvasEventManager {
// 单指拖动更新
this.canvas.on("touch:drag:update", (opt) => {
if (!this.canvas.isDragging || this.editorMode !== OperationType.PAN)
return;
if (!this.canvas.isDragging || this.editorMode !== OperationType.PAN) return;
const currentX = opt.e.touches[0].clientX;
const currentY = opt.e.touches[0].clientY;
@@ -367,10 +352,7 @@ export class CanvasEventManager {
if (this.canvas.isDragging) {
// 使用动画管理器处理惯性效果
if (this.lastMousePositions.length > 1 && opt && opt.e) {
this.animationManager.applyInertiaEffect(
this.lastMousePositions,
isTouch
);
this.animationManager.applyInertiaEffect(this.lastMousePositions, isTouch);
}
}
@@ -405,22 +387,10 @@ export class CanvasEventManager {
});
// 添加对象开始变换时的状态捕获
this.canvas.on(
"object:moving",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:scaling",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:rotating",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:skewing",
this._captureInitialTransformState.bind(this)
);
this.canvas.on("object:moving", this._captureInitialTransformState.bind(this));
this.canvas.on("object:scaling", this._captureInitialTransformState.bind(this));
this.canvas.on("object:rotating", this._captureInitialTransformState.bind(this));
this.canvas.on("object:skewing", this._captureInitialTransformState.bind(this));
this.canvas.on("object:modified", (e) => {
// 移除调试日志
@@ -592,8 +562,7 @@ export class CanvasEventManager {
}
// 验证是否需要合并
const hasExistingObjects =
Array.isArray(activeLayer.fabricObjects) &&
activeLayer.fabricObjects.length > 0;
Array.isArray(activeLayer.fabricObjects) && activeLayer.fabricObjects.length > 0;
const hasNewImage = !!fabricImage;
if (!hasExistingObjects && !hasNewImage) {
@@ -611,10 +580,7 @@ export class CanvasEventManager {
try {
console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
const command = await this.layerManager.LayerObjectsToGroup(
activeLayer,
fabricImage
);
const command = await this.layerManager.LayerObjectsToGroup(activeLayer, fabricImage);
// 设置命令的撤销状态
if (isBoolean(options.undoable)) command.undoable = options.undoable; // 是否撤销
@@ -660,9 +626,7 @@ export class CanvasEventManager {
// 查找对应的图层(现在元素就是图层)
const layer = this.layers.value.find(
(l) =>
l.id === elementId ||
(l.fabricObjects && l.fabricObjects?.[0]?.id === layerId)
(l) => l.fabricObjects && l.fabricObjects?.[0]?.id === layerId
);
if (layer) {
@@ -745,8 +709,7 @@ export class CanvasEventManager {
_detectDeviceType() {
const userAgent = navigator.userAgent.toLowerCase();
const platform = navigator.platform.toLowerCase();
const hasTouchSupport =
"ontouchstart" in window || navigator.maxTouchPoints > 0;
const hasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0;
// 检测操作系统
const isMac = /mac|darwin/.test(platform) || /macintosh/.test(userAgent);

View File

@@ -92,9 +92,7 @@ export class KeyboardManager {
*/
detectTouchDevice() {
return (
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
"ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
);
}
@@ -214,9 +212,7 @@ export class KeyboardManager {
this.container.addEventListener("touchcancel", this._handleTouchEnd);
}
console.log(
`键盘管理器已初始化,平台: ${this.platform}, 触摸设备: ${this.isTouchDevice}`
);
console.log(`键盘管理器已初始化,平台: ${this.platform}, 触摸设备: ${this.isTouchDevice}`);
}
/**
@@ -262,10 +258,7 @@ export class KeyboardManager {
// 调用自定义处理程序
const key = event.key.toLowerCase();
if (
this.customHandlers[key] &&
typeof this.customHandlers[key].onKeyUp === "function"
) {
if (this.customHandlers[key] && typeof this.customHandlers[key].onKeyUp === "function") {
this.customHandlers[key].onKeyUp(event);
}
}
@@ -281,15 +274,11 @@ export class KeyboardManager {
if (touches.length === 2) {
// 双指触摸 - 可用于缩放或调整画笔大小
this.touchState.isTwoFingerTouch = true;
this.touchState.pinchStartDistance = this.getDistanceBetweenTouches(
touches[0],
touches[1]
);
this.touchState.pinchStartDistance = this.getDistanceBetweenTouches(touches[0], touches[1]);
// 如果有画笔管理器,记录起始画笔大小
if (this.toolManager && this.toolManager.brushManager) {
this.touchState.pinchStartBrushSize =
this.toolManager.brushManager.brushSize.value;
this.touchState.pinchStartBrushSize = this.toolManager.brushManager.brushSize.value;
}
} else if (touches.length === 3) {
// 三指触摸 - 可用于撤销/重做
@@ -311,10 +300,7 @@ export class KeyboardManager {
// 双指缩放处理 - 调整画笔大小
if (touches.length === 2 && this.touchState.isTwoFingerTouch) {
const currentDistance = this.getDistanceBetweenTouches(
touches[0],
touches[1]
);
const currentDistance = this.getDistanceBetweenTouches(touches[0], touches[1]);
const scale = currentDistance / this.touchState.pinchStartDistance;
// 调整画笔大小
@@ -587,10 +573,7 @@ export class KeyboardManager {
let shortcutKey = "";
// 统一处理Mac和PC的修饰键
if (
(this.platform === "mac" && event.metaKey) ||
(this.platform !== "mac" && event.ctrlKey)
) {
if ((this.platform === "mac" && event.metaKey) || (this.platform !== "mac" && event.ctrlKey)) {
shortcutKey += `${this.modifierKeys.cmdOrCtrl}+`;
} else if (event.ctrlKey) {
shortcutKey += "ctrl+";

View File

@@ -216,9 +216,7 @@ export class EnhancedLiquifyManager {
// 计算图像大小
const pixelCount = imageData.width * imageData.height;
console.log(
`液化选择渲染器: 图像大小=${pixelCount}像素, WebGL可用=${this.isWebGLAvailable}`
);
console.log(`液化选择渲染器: 图像大小=${pixelCount}像素, WebGL可用=${this.isWebGLAvailable}`);
// 默认使用CPU渲染器
this.activeRenderer = this.cpuRenderer;
@@ -249,11 +247,7 @@ export class EnhancedLiquifyManager {
this.activeRenderer = this.webglRenderer;
this.renderMode = "webgl";
} else {
console.log(
`液化功能: 使用CPU渲染模式${
!this.isWebGLAvailable ? " (WebGL不可用)" : ""
}`
);
console.log(`液化功能: 使用CPU渲染模式${!this.isWebGLAvailable ? " (WebGL不可用)" : ""}`);
}
}
@@ -318,16 +312,11 @@ export class EnhancedLiquifyManager {
this.params[param] = value;
// 同步更新当前渲染器 - 关键修复:确保参数正确传递
if (
this.activeRenderer &&
typeof this.activeRenderer.setParam === "function"
) {
if (this.activeRenderer && typeof this.activeRenderer.setParam === "function") {
console.log(`EnhancedLiquifyManager 设置参数: ${param}=${value}`);
this.activeRenderer.setParam(param, value);
} else {
console.warn(
`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`
);
console.warn(`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`);
}
return true;
@@ -382,25 +371,17 @@ export class EnhancedLiquifyManager {
* @param {Number} y 初始Y坐标
*/
startLiquifyOperation(x, y) {
if (
this.activeRenderer &&
typeof this.activeRenderer.startDeformation === "function"
) {
if (this.activeRenderer && typeof this.activeRenderer.startDeformation === "function") {
this.activeRenderer.startDeformation(x, y);
}
console.log(
`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`
);
console.log(`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`);
}
/**
* 结束液化操作
*/
endLiquifyOperation() {
if (
this.activeRenderer &&
typeof this.activeRenderer.endDeformation === "function"
) {
if (this.activeRenderer && typeof this.activeRenderer.endDeformation === "function") {
this.activeRenderer.endDeformation();
}
console.log(`结束液化操作,渲染模式=${this.renderMode}`);
@@ -443,9 +424,7 @@ export class EnhancedLiquifyManager {
// 坐标边界检查
if (x < 0 || x >= imageWidth || y < 0 || y >= imageHeight) {
console.warn(
`液化坐标超出图像范围: (${x}, ${y}), 图像尺寸: ${imageWidth}x${imageHeight}`
);
console.warn(`液化坐标超出图像范围: (${x}, ${y}), 图像尺寸: ${imageWidth}x${imageHeight}`);
return null;
}
@@ -510,9 +489,7 @@ export class EnhancedLiquifyManager {
console.log(
`液化性能数据: 模式=${this.renderMode}, 平均耗时=${avgTime.toFixed(
2
)}ms, 图像尺寸=${this.originalImageData?.width}x${
this.originalImageData?.height
}`
)}ms, 图像尺寸=${this.originalImageData?.width}x${this.originalImageData?.height}`
);
}
@@ -604,8 +581,7 @@ export class EnhancedLiquifyManager {
const singleObject = objectsToCheck.length === 1;
const isImage =
singleObject &&
(objectsToCheck[0].type === "image" ||
objectsToCheck[0].type === "rasterized-layer");
(objectsToCheck[0].type === "image" || objectsToCheck[0].type === "rasterized-layer");
// 检查是否为组
const isGroup =
@@ -655,19 +631,14 @@ export class EnhancedLiquifyManager {
tempCanvas.height = fabricObject.height;
const tempCtx = tempCanvas.getContext("2d");
console.log(
`创建临时Canvas尺寸: ${tempCanvas.width}x${tempCanvas.height}`
);
console.log(`创建临时Canvas尺寸: ${tempCanvas.width}x${tempCanvas.height}`);
// 处理不同的图像源
if (fabricObject._element) {
console.log("使用 _element 绘制图像");
// 检查_element是否有效
if (
!fabricObject._element.complete &&
fabricObject._element.tagName === "IMG"
) {
if (!fabricObject._element.complete && fabricObject._element.tagName === "IMG") {
console.log("图像未加载完成,等待加载...");
fabricObject._element.onload = () => {
try {
@@ -678,12 +649,7 @@ export class EnhancedLiquifyManager {
fabricObject.width,
fabricObject.height
);
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log("✅ 图像加载完成后获取数据成功");
resolve(imageData);
} catch (error) {
@@ -698,17 +664,8 @@ export class EnhancedLiquifyManager {
}
// 直接绘制已加载的图像
tempCtx.drawImage(
fabricObject._element,
0,
0,
fabricObject.width,
fabricObject.height
);
} else if (
fabricObject.getSrc &&
typeof fabricObject.getSrc === "function"
) {
tempCtx.drawImage(fabricObject._element, 0, 0, fabricObject.width, fabricObject.height);
} else if (fabricObject.getSrc && typeof fabricObject.getSrc === "function") {
console.log("使用 getSrc() 方法获取图像源");
// 通过URL创建图像
@@ -717,24 +674,11 @@ export class EnhancedLiquifyManager {
img.onload = () => {
try {
console.log(
`图像加载成功,原始尺寸: ${img.naturalWidth}x${img.naturalHeight}`
);
console.log(`图像加载成功,原始尺寸: ${img.naturalWidth}x${img.naturalHeight}`);
tempCtx.drawImage(
img,
0,
0,
fabricObject.width,
fabricObject.height
);
tempCtx.drawImage(img, 0, 0, fabricObject.width, fabricObject.height);
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log("✅ 通过URL获取图像数据成功");
resolve(imageData);
@@ -762,20 +706,9 @@ export class EnhancedLiquifyManager {
img.onload = () => {
try {
tempCtx.drawImage(
img,
0,
0,
fabricObject.width,
fabricObject.height
);
tempCtx.drawImage(img, 0, 0, fabricObject.width, fabricObject.height);
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log("✅ 通过src属性获取图像数据成功");
resolve(imageData);
@@ -795,20 +728,13 @@ export class EnhancedLiquifyManager {
return;
} else {
console.error("无法找到有效的图像源");
reject(
new Error("图像对象缺少有效的图像源_element, getSrc, 或 src")
);
reject(new Error("图像对象缺少有效的图像源_element, getSrc, 或 src"));
return;
}
// 如果走到这里说明使用了_element直接绘制
try {
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
`✅ 获取图像数据成功: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +

View File

@@ -178,10 +178,7 @@ export class HybridLiquifyManager {
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
const baseRotationSpeed = 0.015;
const rotationAngle =
(clockwise ? 1 : -1) *
baseRotationSpeed *
strength *
(1.0 + timeFactor * 0.3);
(clockwise ? 1 : -1) * baseRotationSpeed * strength * (1.0 + timeFactor * 0.3);
this.accumulatedRotation += rotationAngle;
@@ -209,13 +206,7 @@ export class HybridLiquifyManager {
const sourceY = centerY + Math.sin(newAngle) * distance;
// 双线性插值采样
const color = this._bilinearSample(
srcData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(srcData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -232,16 +223,7 @@ export class HybridLiquifyManager {
/**
* 像素级水晶效果
*/
_applyPixelCrystal(
srcData,
dstData,
width,
height,
centerX,
centerY,
radius,
strength
) {
_applyPixelCrystal(srcData, dstData, width, height, centerX, centerY, radius, strength) {
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
const distortionStrength = strength * (1.0 + timeFactor * 0.5);
@@ -266,25 +248,16 @@ export class HybridLiquifyManager {
// 多层波浪扭曲
const wave1 = Math.sin(angle * 8 + this.pressDuration * 0.005) * 0.6;
const wave2 = Math.cos(angle * 12 + this.pressDuration * 0.003) * 0.4;
const waveAngle =
angle + (wave1 + wave2) * distortionStrength * falloff;
const waveAngle = angle + (wave1 + wave2) * distortionStrength * falloff;
const radialMod =
1 +
Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) *
0.3;
1 + Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) * 0.3;
const modDistance = distance * radialMod;
const sourceX = centerX + Math.cos(waveAngle) * modDistance;
const sourceY = centerY + Math.sin(waveAngle) * modDistance;
const color = this._bilinearSample(
srcData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(srcData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -311,16 +284,7 @@ export class HybridLiquifyManager {
/**
* 像素级边缘效果
*/
_applyPixelEdge(
srcData,
dstData,
width,
height,
centerX,
centerY,
radius,
strength
) {
_applyPixelEdge(srcData, dstData, width, height, centerX, centerY, radius, strength) {
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
const edgeStrength = strength * (1.0 + timeFactor * 0.4);
@@ -354,13 +318,7 @@ export class HybridLiquifyManager {
const sourceX = x + offsetX;
const sourceY = y + offsetY;
const color = this._bilinearSample(
srcData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(srcData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;

View File

@@ -79,12 +79,7 @@ export class LiquifyCPUManager {
this.canvas.width = imageSource.width;
this.canvas.height = imageSource.height;
this.ctx.drawImage(imageSource, 0, 0);
this.originalImageData = this.ctx.getImageData(
0,
0,
imageSource.width,
imageSource.height
);
this.originalImageData = this.ctx.getImageData(0, 0, imageSource.width, imageSource.height);
} else {
throw new Error("不支持的图像类型");
}
@@ -95,10 +90,7 @@ export class LiquifyCPUManager {
this.originalImageData.height
);
this._initMesh(
this.originalImageData.width,
this.originalImageData.height
);
this._initMesh(this.originalImageData.width, this.originalImageData.height);
this.initialized = true;
return true;
} catch (error) {
@@ -268,13 +260,7 @@ export class LiquifyCPUManager {
* @param {number} strength 强度
* @param {boolean} isClockwise 是否顺时针旋转
*/
_applyEnhancedRotationDeformation(
centerX,
centerY,
radius,
strength,
isClockwise
) {
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
if (!this.currentImageData) return;
const data = this.currentImageData.data;
@@ -287,11 +273,7 @@ export class LiquifyCPUManager {
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
const baseRotationSpeed = 0.02; // 使用与测试文件相同的速度
const rotationAngle =
(isClockwise ? 1 : -1) *
baseRotationSpeed *
pressure *
power *
(1.0 + timeFactor * 0.5);
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
// 累积旋转角度 - 关键:这确保了持续旋转效果
this.accumulatedRotation += rotationAngle;
@@ -322,13 +304,7 @@ export class LiquifyCPUManager {
const sourceY = centerY + Math.sin(newAngle) * distance;
// 双线性插值采样 - 确保像素连续性
const color = this._bilinearSample(
tempData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -389,13 +365,7 @@ export class LiquifyCPUManager {
const sourceY = centerY + dy * scale;
// 双线性插值采样
const color = this._bilinearSample(
tempData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -461,13 +431,7 @@ export class LiquifyCPUManager {
const sourceX = x - pushX;
const sourceY = y - pushY;
const color = this._bilinearSample(
tempData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -504,13 +468,7 @@ export class LiquifyCPUManager {
const sourceX = x - offsetX;
const sourceY = y - offsetY;
const color = this._bilinearSample(
tempData,
width,
height,
sourceX,
sourceY
);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -561,7 +519,7 @@ export class LiquifyCPUManager {
this._applyEnhancedPushDeformation(x, y, radius, strength);
break;
default:
default: {
// 对于其他模式,使用原有的网格算法
if (!this.mesh) return;
@@ -569,20 +527,14 @@ export class LiquifyCPUManager {
const timeFactor = Math.min(this.pressDuration / 1000, 4.0);
const finalStrength = baseStrength * (1.0 + timeFactor * 0.5);
this._applyDeformation(
x,
y,
radius,
finalStrength,
mode,
this.params.distortion
);
this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion);
if (this.config.smoothingIterations > 0) {
this._lightSmoothing();
}
return this._applyMeshToImage();
}
}
// 对于像素算法,直接返回当前图像数据
@@ -592,96 +544,96 @@ export class LiquifyCPUManager {
/**
* 应用液化变形 - 主要入口,集成增强算法
*/
applyDeformation(x, y) {
if (!this.initialized || !this.originalImageData) {
console.warn("液化管理器未初始化或缺少必要数据");
return this.currentImageData;
}
// applyDeformation(x, y) {
// if (!this.initialized || !this.originalImageData) {
// console.warn("液化管理器未初始化或缺少必要数据");
// return this.currentImageData;
// }
// 更新鼠标位置
this.currentMouseX = x;
this.currentMouseY = y;
// // 更新鼠标位置
// this.currentMouseX = x;
// this.currentMouseY = y;
// 计算拖拽参数
const deltaX = this.currentMouseX - this.initialMouseX;
const deltaY = this.currentMouseY - this.initialMouseY;
this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
this.dragAngle = Math.atan2(deltaY, deltaX);
// // 计算拖拽参数
// const deltaX = this.currentMouseX - this.initialMouseX;
// const deltaY = this.currentMouseY - this.initialMouseY;
// this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// this.dragAngle = Math.atan2(deltaY, deltaX);
// 获取当前参数
const { size, pressure, power } = this.params;
const mode = this.currentMode;
const radius = size;
const strength = pressure * power;
// // 获取当前参数
// const { size, pressure, power } = this.params;
// const mode = this.currentMode;
// const radius = size;
// const strength = pressure * power;
// 根据模式选择算法
const pixelModes = [
this.modes.CLOCKWISE,
this.modes.COUNTERCLOCKWISE,
this.modes.PINCH,
this.modes.EXPAND,
this.modes.PUSH,
];
// // 根据模式选择算法
// const pixelModes = [
// this.modes.CLOCKWISE,
// this.modes.COUNTERCLOCKWISE,
// this.modes.PINCH,
// this.modes.EXPAND,
// this.modes.PUSH,
// ];
if (pixelModes.includes(mode)) {
// 使用增强的像素算法
switch (mode) {
case this.modes.CLOCKWISE:
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
break;
case this.modes.COUNTERCLOCKWISE:
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
break;
case this.modes.PINCH:
this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
break;
case this.modes.EXPAND:
this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
break;
case this.modes.PUSH:
this._applyEnhancedPushDeformation(x, y, radius, strength);
break;
}
// if (pixelModes.includes(mode)) {
// // 使用增强的像素算法
// switch (mode) {
// case this.modes.CLOCKWISE:
// this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
// break;
// case this.modes.COUNTERCLOCKWISE:
// this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
// break;
// case this.modes.PINCH:
// this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
// break;
// case this.modes.EXPAND:
// this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
// break;
// case this.modes.PUSH:
// this._applyEnhancedPushDeformation(x, y, radius, strength);
// break;
// }
// 更新最后应用时间
this.lastApplyTime = Date.now();
this.isFirstApply = false;
// // 更新最后应用时间
// this.lastApplyTime = Date.now();
// this.isFirstApply = false;
return this.currentImageData;
} else {
// 使用原有的网格算法处理其他模式
if (!this.mesh) {
console.warn("网格未初始化");
return this.currentImageData;
}
// return this.currentImageData;
// } else {
// // 使用原有的网格算法处理其他模式
// if (!this.mesh) {
// console.warn("网格未初始化");
// return this.currentImageData;
// }
const finalStrength = (strength * this.config.maxStrength) / 100;
// const finalStrength = (strength * this.config.maxStrength) / 100;
// 应用变形
this._applyDeformation(
x,
y,
radius,
finalStrength,
mode,
this.params.distortion
);
// // 应用变形
// this._applyDeformation(
// x,
// y,
// radius,
// finalStrength,
// mode,
// this.params.distortion,
// );
// 平滑处理
if (this.config.smoothingIterations > 0) {
this._smoothMesh();
}
// // 平滑处理
// if (this.config.smoothingIterations > 0) {
// this._smoothMesh();
// }
// 更新图像数据
const result = this._applyMeshToImage();
// // 更新图像数据
// const result = this._applyMeshToImage();
// 更新最后应用时间
this.lastApplyTime = Date.now();
this.isFirstApply = false;
// // 更新最后应用时间
// this.lastApplyTime = Date.now();
// this.isFirstApply = false;
return result;
}
}
// return result;
// }
// }
/**
* 双线性插值采样 - 用于像素级算法
@@ -773,22 +725,14 @@ export class LiquifyCPUManager {
const baseDistortion = Math.max(distortion, 0.3);
const timeFactor = Math.min(this.pressDuration / 1000, 2.0);
const timeEnhancedDistortion =
baseDistortion * (1.0 + timeFactor * 0.3);
const timeEnhancedDistortion = baseDistortion * (1.0 + timeFactor * 0.3);
const wave1 =
Math.sin(crystalAngle * 8 + this.pressDuration * 0.005) * 0.6;
const wave2 =
Math.cos(crystalAngle * 12 + this.pressDuration * 0.003) * 0.4;
const waveAngle =
crystalAngle + (wave1 + wave2) * timeEnhancedDistortion;
const wave1 = Math.sin(crystalAngle * 8 + this.pressDuration * 0.005) * 0.6;
const wave2 = Math.cos(crystalAngle * 12 + this.pressDuration * 0.003) * 0.4;
const waveAngle = crystalAngle + (wave1 + wave2) * timeEnhancedDistortion;
const radialMod =
1 +
Math.sin(
crystalRadius * Math.PI * 2 + this.pressDuration * 0.002
) *
0.3;
1 + Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) * 0.3;
const modDistance = distance * radialMod;
const crystalX = x + Math.cos(waveAngle) * modDistance;
@@ -809,8 +753,7 @@ export class LiquifyCPUManager {
const baseEdgeDistortion = Math.max(distortion, 0.5);
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
const timeEnhancedDistortion =
baseEdgeDistortion * (1.0 + timeFactor * 0.4);
const timeEnhancedDistortion = baseEdgeDistortion * (1.0 + timeFactor * 0.4);
const edgeWave =
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
@@ -887,11 +830,7 @@ export class LiquifyCPUManager {
const points = this.mesh.deformedPoints;
const tempPoints = points.map((p) => ({ x: p.x, y: p.y }));
for (
let iteration = 0;
iteration < this.config.smoothingIterations;
iteration++
) {
for (let iteration = 0; iteration < this.config.smoothingIterations; iteration++) {
for (let y = 1; y < rows; y++) {
for (let x = 1; x < cols; x++) {
const idx = y * (cols + 1) + x;
@@ -996,19 +935,8 @@ export class LiquifyCPUManager {
for (let x = 0; x < width; x += step) {
const srcPos = this._mapPointBack(x, y);
if (
srcPos.x >= 0 &&
srcPos.x < width &&
srcPos.y >= 0 &&
srcPos.y < height
) {
const color = this._bilinearInterpolate(
srcData,
width,
height,
srcPos.x,
srcPos.y
);
if (srcPos.x >= 0 && srcPos.x < width && srcPos.y >= 0 && srcPos.y < height) {
const color = this._bilinearInterpolate(srcData, width, height, srcPos.x, srcPos.y);
// 如果使用步长采样,需要填充相邻像素
for (let dy = 0; dy < step && y + dy < height; dy++) {
@@ -1070,22 +998,14 @@ export class LiquifyCPUManager {
// 根据推拉模式和拖拽距离动态调整强度
let strength;
if (mode === this.modes.PUSH) {
const baseStrength =
(pressure * power * this.config.maxStrength) / 100;
const baseStrength = (pressure * power * this.config.maxStrength) / 100;
const distanceFactor = Math.min(this.dragDistance / radius, 2.0);
strength = baseStrength * distanceFactor * 0.3; // 批量处理时降低强度
} else {
strength = (pressure * power * this.config.maxStrength) / 100;
}
this._applyDeformation(
pos.x,
pos.y,
radius,
strength,
mode,
distortion
);
this._applyDeformation(pos.x, pos.y, radius, strength, mode, distortion);
});
// 结束拖拽操作
@@ -1331,14 +1251,7 @@ export class LiquifyCPUManager {
const finalStrength = (strength * this.config.maxStrength) / 100;
// 应用变形
this._applyDeformation(
x,
y,
radius,
finalStrength,
mode,
this.params.distortion
);
this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion);
// 平滑处理
if (this.config.smoothingIterations > 0) {

View File

@@ -130,32 +130,18 @@ export class LiquifyManager {
return null;
}
console.log(
`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`,
params
);
console.log(`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
try {
// 直接调用EnhancedLiquifyManager的applyLiquify方法
// 避免重复设置参数让EnhancedLiquifyManager处理参数设置
const resultData = await this.enhancedManager.applyLiquify(
targetObject,
mode,
params,
x,
y
);
const resultData = await this.enhancedManager.applyLiquify(targetObject, mode, params, x, y);
// 确保返回结果数据
if (!resultData) {
console.warn("液化变形没有返回结果数据");
} else {
console.log(
"✅ 液化变形成功,返回图像数据尺寸:",
resultData.width,
"x",
resultData.height
);
console.log("✅ 液化变形成功,返回图像数据尺寸:", resultData.width, "x", resultData.height);
}
return resultData;

View File

@@ -159,9 +159,7 @@ export class LiquifyRealTimeUpdater {
// });
// }
} else {
console.warn(
"=================快速更新液化效果时,图像数据未变化,跳过更新"
);
console.warn("=================快速更新液化效果时,图像数据未变化,跳过更新");
}
} catch (error) {
console.error("快速更新液化效果失败:", error);
@@ -184,6 +182,8 @@ export class LiquifyRealTimeUpdater {
*/
async _fullUpdate(imageData) {
return new Promise((resolve, reject) => {
// 临时禁用画布自动渲染
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
try {
// 使用高质量canvas进行最终渲染
this.highQualityCtx.putImageData(imageData, 0, 0);
@@ -233,9 +233,6 @@ export class LiquifyRealTimeUpdater {
selected: false,
evented: originalObj.evented,
});
// 临时禁用画布自动渲染
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
this.canvas.renderOnAddRemove = false;
// 智能查找和替换canvas上的对象
@@ -244,9 +241,7 @@ export class LiquifyRealTimeUpdater {
// 如果直接查找失败尝试通过ID查找
if (targetIndex === -1 && originalObjId) {
targetIndex = allObjects.findIndex(
(obj) => obj.id === originalObjId
);
targetIndex = allObjects.findIndex((obj) => obj.id === originalObjId);
if (targetIndex !== -1) {
console.log(`通过ID找到目标对象: ${originalObjId}`);
// 更新目标对象引用
@@ -256,9 +251,7 @@ export class LiquifyRealTimeUpdater {
// 如果通过ID查找仍然失败尝试通过图层ID查找
if (targetIndex === -1 && originalObjLayerId) {
targetIndex = allObjects.findIndex(
(obj) => obj.layerId === originalObjLayerId
);
targetIndex = allObjects.findIndex((obj) => obj.layerId === originalObjLayerId);
if (targetIndex !== -1) {
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
// 更新目标对象引用
@@ -284,9 +277,7 @@ export class LiquifyRealTimeUpdater {
resolve(newImg);
} else {
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
console.warn(
"在画布中找不到目标对象,可能已被其他操作移除或替换"
);
console.warn("在画布中找不到目标对象,可能已被其他操作移除或替换");
// 恢复自动渲染设置
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
@@ -333,24 +324,6 @@ export class LiquifyRealTimeUpdater {
}
}
/**
* 清理资源
*/
dispose() {
this.targetObject = null;
this.cachedDataURL = null;
this.pendingImageData = null;
this.updateQueue.length = 0;
// 清理临时canvas
if (this.tempCanvas) {
this.tempCanvas.width = 0;
this.tempCanvas.height = 0;
this.tempCanvas = null;
this.tempCtx = null;
}
}
/**
* 获取当前目标对象
* @returns {Object} 当前的fabric对象
@@ -388,13 +361,6 @@ export class LiquifyRealTimeUpdater {
console.log("✅ 恢复正常渲染模式");
}
/**
* 获取当前目标对象
*/
getTargetObject() {
return this.targetObject;
}
/**
* 设置图像质量
* @param {Number} quality 质量值 (0.1-1.0)
@@ -416,6 +382,24 @@ export class LiquifyRealTimeUpdater {
}
}
// /**
// * 清理资源
// */
// dispose() {
// this.targetObject = null;
// this.cachedDataURL = null;
// this.pendingImageData = null;
// this.updateQueue.length = 0;
// // 清理临时canvas
// if (this.tempCanvas) {
// this.tempCanvas.width = 0;
// this.tempCanvas.height = 0;
// this.tempCanvas = null;
// this.tempCtx = null;
// }
// }
/**
* 清理资源
*/

View File

@@ -30,10 +30,7 @@ export class LiquifyStateManager {
// 设备性能检测
this.devicePerformance = this._detectDevicePerformance();
console.log(
"🎯 液化状态管理器已初始化,设备性能等级:",
this.devicePerformance
);
console.log("🎯 液化状态管理器已初始化,设备性能等级:", this.devicePerformance);
}
/**
@@ -129,17 +126,10 @@ export class LiquifyStateManager {
this.performanceMetrics.totalOperations++;
this.performanceMetrics.totalTime += operationTime;
this.performanceMetrics.averageTime =
this.performanceMetrics.totalTime /
this.performanceMetrics.totalOperations;
this.performanceMetrics.totalTime / this.performanceMetrics.totalOperations;
this.performanceMetrics.maxTime = Math.max(
this.performanceMetrics.maxTime,
operationTime
);
this.performanceMetrics.minTime = Math.min(
this.performanceMetrics.minTime,
operationTime
);
this.performanceMetrics.maxTime = Math.max(this.performanceMetrics.maxTime, operationTime);
this.performanceMetrics.minTime = Math.min(this.performanceMetrics.minTime, operationTime);
this.performanceMetrics.lastOperationTime = operationTime;
}
@@ -148,15 +138,8 @@ export class LiquifyStateManager {
* @param {Object} metrics 性能指标对象
*/
recordOperationMetrics(metrics) {
const {
operationTime,
operationType,
mode,
coordinates,
imageSize,
renderMode,
isRealTime,
} = metrics;
const { operationTime, operationType, mode, coordinates, imageSize, renderMode, isRealTime } =
metrics;
// 记录基础性能数据
this.recordDeformation(operationTime);
@@ -195,9 +178,7 @@ export class LiquifyStateManager {
// 降低图像质量
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
if (currentQuality > 0.7) {
this.realtimeUpdater.setImageQuality(
Math.max(0.7, currentQuality - 0.1)
);
this.realtimeUpdater.setImageQuality(Math.max(0.7, currentQuality - 0.1));
console.log("⚡ 自动降低图像质量以提升性能");
}
@@ -215,9 +196,7 @@ export class LiquifyStateManager {
if (operationTime < 20 && this.devicePerformance === "high") {
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
if (currentQuality < 1.0) {
this.realtimeUpdater.setImageQuality(
Math.min(1.0, currentQuality + 0.05)
);
this.realtimeUpdater.setImageQuality(Math.min(1.0, currentQuality + 0.05));
}
}
}
@@ -375,8 +354,7 @@ export class LiquifyStateManager {
// 恢复原始设置
this.canvas.renderOnAddRemove = this._originalSettings.renderOnAddRemove;
this.canvas.skipOffscreen = this._originalSettings.skipOffscreen;
this.canvas.enableRetinaScaling =
this._originalSettings.enableRetinaScaling;
this.canvas.enableRetinaScaling = this._originalSettings.enableRetinaScaling;
this._originalSettings = null;
}
@@ -389,14 +367,8 @@ export class LiquifyStateManager {
this.performanceMetrics.averageTime =
this.performanceMetrics.totalTime / (this.operationCount + 1);
this.performanceMetrics.maxTime = Math.max(
this.performanceMetrics.maxTime,
operationTime
);
this.performanceMetrics.minTime = Math.min(
this.performanceMetrics.minTime,
operationTime
);
this.performanceMetrics.maxTime = Math.max(this.performanceMetrics.maxTime, operationTime);
this.performanceMetrics.minTime = Math.min(this.performanceMetrics.minTime, operationTime);
}
/**

View File

@@ -61,8 +61,7 @@ export class LiquifyWebGLManager {
static isSupported() {
try {
const canvas = document.createElement("canvas");
const gl =
canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return !!gl;
} catch (e) {
return false;
@@ -76,9 +75,7 @@ export class LiquifyWebGLManager {
try {
// 创建WebGL画布
this.canvas = document.createElement("canvas");
this.gl =
this.canvas.getContext("webgl") ||
this.canvas.getContext("experimental-webgl");
this.gl = this.canvas.getContext("webgl") || this.canvas.getContext("experimental-webgl");
if (!this.gl) {
throw new Error("WebGL不可用");
@@ -200,10 +197,7 @@ export class LiquifyWebGLManager {
}
`;
this.program = this._createProgram(
vertexShaderSource,
fragmentShaderSource
);
this.program = this._createProgram(vertexShaderSource, fragmentShaderSource);
}
/**
@@ -228,10 +222,7 @@ export class LiquifyWebGLManager {
_createProgram(vertexSource, fragmentSource) {
const gl = this.gl;
const vertexShader = this._createShader(gl.VERTEX_SHADER, vertexSource);
const fragmentShader = this._createShader(
gl.FRAGMENT_SHADER,
fragmentSource
);
const fragmentShader = this._createShader(gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
@@ -267,9 +258,7 @@ export class LiquifyWebGLManager {
*/
_createMeshBuffer() {
const gl = this.gl;
const vertices = new Float32Array([
-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1,
]);
const vertices = new Float32Array([-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1]);
this.meshBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.meshBuffer);
@@ -427,12 +416,8 @@ export class LiquifyWebGLManager {
}
// 计算拖拽向量(用于推拉模式)
const dragX = this.isDragging
? (x - this.initialMouseX) / this.canvas.width
: 0;
const dragY = this.isDragging
? -(y - this.initialMouseY) / this.canvas.height
: 0;
const dragX = this.isDragging ? (x - this.initialMouseX) / this.canvas.width : 0;
const dragY = this.isDragging ? -(y - this.initialMouseY) / this.canvas.height : 0;
// 获取模式索引
const modeIndex = Object.values(this.modes).indexOf(this.currentMode);
@@ -466,22 +451,13 @@ export class LiquifyWebGLManager {
// 读取结果并转换为ImageData
const pixels = new Uint8Array(this.canvas.width * this.canvas.height * 4);
gl.readPixels(
0,
0,
this.canvas.width,
this.canvas.height,
gl.RGBA,
gl.UNSIGNED_BYTE,
pixels
);
gl.readPixels(0, 0, this.canvas.width, this.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// 翻转Y轴以匹配ImageData格式
const flippedPixels = new Uint8ClampedArray(pixels.length);
for (let y = 0; y < this.canvas.height; y++) {
for (let x = 0; x < this.canvas.width; x++) {
const srcIndex =
((this.canvas.height - 1 - y) * this.canvas.width + x) * 4;
const srcIndex = ((this.canvas.height - 1 - y) * this.canvas.width + x) * 4;
const dstIndex = (y * this.canvas.width + x) * 4;
flippedPixels[dstIndex] = pixels[srcIndex];
flippedPixels[dstIndex + 1] = pixels[srcIndex + 1];
@@ -503,16 +479,7 @@ export class LiquifyWebGLManager {
// 将原始纹理复制到当前纹理
gl.bindTexture(gl.TEXTURE_2D, this.currentTexture);
gl.copyTexImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
0,
0,
this.canvas.width,
this.canvas.height,
0
);
gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, this.canvas.width, this.canvas.height, 0);
// 读取原始纹理数据
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
@@ -525,21 +492,9 @@ export class LiquifyWebGLManager {
);
const pixels = new Uint8Array(this.canvas.width * this.canvas.height * 4);
gl.readPixels(
0,
0,
this.canvas.width,
this.canvas.height,
gl.RGBA,
gl.UNSIGNED_BYTE,
pixels
);
gl.readPixels(0, 0, this.canvas.width, this.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return new ImageData(
new Uint8ClampedArray(pixels),
this.canvas.width,
this.canvas.height
);
return new ImageData(new Uint8ClampedArray(pixels), this.canvas.width, this.canvas.height);
}
/**

View File

@@ -144,10 +144,7 @@ export class MinimapManager {
this.eventHandlers.mouseup = this.handleMinimapMouseUp;
// 移除mouseout事件处理允许拖动操作持续到鼠标释放
this.minimapCanvas.addEventListener(
"mousedown",
this.eventHandlers.mousedown
);
this.minimapCanvas.addEventListener("mousedown", this.eventHandlers.mousedown);
document.addEventListener("mousemove", this.eventHandlers.mousemove);
document.addEventListener("mouseup", this.eventHandlers.mouseup);
// 移除mouseout事件监听
@@ -177,10 +174,7 @@ export class MinimapManager {
this.eventHandlers.touchend = this.handleMinimapMouseUp;
this.minimapCanvas.addEventListener(
"touchstart",
this.eventHandlers.touchstart
);
this.minimapCanvas.addEventListener("touchstart", this.eventHandlers.touchstart);
document.addEventListener("touchmove", this.eventHandlers.touchmove, {
passive: false,
});
@@ -204,18 +198,12 @@ export class MinimapManager {
this.mainCanvas.off("object:rotating", this.handleMainCanvasChange);
// 移除鼠标事件监听
this.minimapCanvas.removeEventListener(
"mousedown",
this.eventHandlers.mousedown
);
this.minimapCanvas.removeEventListener("mousedown", this.eventHandlers.mousedown);
document.removeEventListener("mousemove", this.eventHandlers.mousemove);
document.removeEventListener("mouseup", this.eventHandlers.mouseup);
// 移除触摸事件监听
this.minimapCanvas.removeEventListener(
"touchstart",
this.eventHandlers.touchstart
);
this.minimapCanvas.removeEventListener("touchstart", this.eventHandlers.touchstart);
document.removeEventListener("touchmove", this.eventHandlers.touchmove);
document.removeEventListener("touchend", this.eventHandlers.touchend);
}
@@ -281,8 +269,7 @@ export class MinimapManager {
}
// 添加边距
const padding =
Math.max(this.mainCanvas.getWidth(), this.mainCanvas.getHeight()) * 0.1;
const padding = Math.max(this.mainCanvas.getWidth(), this.mainCanvas.getHeight()) * 0.1;
this.contentBounds = {
minX: minX - padding,
minY: minY - padding,
@@ -347,11 +334,7 @@ export class MinimapManager {
this.dragStart = { x, y };
// 移动画布视口
this.moveViewport(
this.dragStartViewport.x + deltaX,
this.dragStartViewport.y + deltaY,
false
);
this.moveViewport(this.dragStartViewport.x + deltaX, this.dragStartViewport.y + deltaY, false);
// 更新拖拽起始视口位置
this.dragStartViewport = this.calculateViewportRect();
@@ -391,9 +374,7 @@ export class MinimapManager {
viewportHeight = this.lastViewportSize.height;
} else {
viewportWidth = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX);
viewportHeight = Math.round(
(this.mainCanvas.getHeight() / zoom) * scaleY
);
viewportHeight = Math.round((this.mainCanvas.getHeight() / zoom) * scaleY);
}
// 添加边界限制,确保视口不会超出小地图
@@ -476,21 +457,11 @@ export class MinimapManager {
try {
// 清空小地图
this.minimapCtx.clearRect(
0,
0,
this.minimapSize.width,
this.minimapSize.height
);
this.minimapCtx.clearRect(0, 0, this.minimapSize.width, this.minimapSize.height);
// 绘制小地图背景
this.minimapCtx.fillStyle = this.mainCanvas.backgroundColor || "#f0f0f0";
this.minimapCtx.fillRect(
0,
0,
this.minimapSize.width,
this.minimapSize.height
);
this.minimapCtx.fillRect(0, 0, this.minimapSize.width, this.minimapSize.height);
// 计算内容边界尺寸
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
@@ -654,11 +625,7 @@ export class MinimapManager {
this.removeEventListeners();
// 从DOM中移除canvas
if (
this.container &&
this.minimapCanvas &&
this.minimapCanvas.parentNode === this.container
) {
if (this.container && this.minimapCanvas && this.minimapCanvas.parentNode === this.container) {
this.container.removeChild(this.minimapCanvas);
}
@@ -709,12 +676,7 @@ export class MinimapManager {
}
const offCtx = this._offscreenCanvas.getContext("2d");
offCtx.clearRect(
0,
0,
this._offscreenCanvas.width,
this._offscreenCanvas.height
);
offCtx.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height);
// 绘制图层内容到离屏画布
this._renderLayersToMinimap(offCtx, ratio);
@@ -755,8 +717,8 @@ export class MinimapManager {
typeof this.canvas.layers.value !== "undefined"
? this.canvas.layers.value
: Array.isArray(this.canvas.layers)
? this.canvas.layers
: [];
? this.canvas.layers
: [];
// 过滤出可见图层
layersArray.forEach((layer) => {
@@ -800,10 +762,8 @@ export class MinimapManager {
const left = fabricObj.left * ratio;
const top = fabricObj.top * ratio;
const width =
(fabricObj.width || 20) * (fabricObj.scaleX || 1) * ratio;
const height =
(fabricObj.height || 20) * (fabricObj.scaleY || 1) * ratio;
const width = (fabricObj.width || 20) * (fabricObj.scaleX || 1) * ratio;
const height = (fabricObj.height || 20) * (fabricObj.scaleY || 1) * ratio;
ctx.fillRect(left, top, width, height);
}

View File

@@ -319,10 +319,7 @@ export class SelectionManager {
}
// 触发选区变化回调
if (
this.onSelectionChanged &&
typeof this.onSelectionChanged === "function"
) {
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
this.onSelectionChanged();
}
@@ -397,10 +394,7 @@ export class SelectionManager {
this.featherAmount = 0;
// 触发选区变化回调
if (
this.onSelectionChanged &&
typeof this.onSelectionChanged === "function"
) {
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
this.onSelectionChanged();
}
@@ -600,8 +594,7 @@ export class SelectionManager {
// 添加新的点,但避免添加过于密集的点
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
const distance = Math.sqrt(
Math.pow(pointer.x - lastPoint.x, 2) +
Math.pow(pointer.y - lastPoint.y, 2)
Math.pow(pointer.x - lastPoint.x, 2) + Math.pow(pointer.y - lastPoint.y, 2)
);
// 只有当距离大于2像素时才添加新点避免路径过于复杂
@@ -653,8 +646,7 @@ export class SelectionManager {
const firstPoint = this.drawingPoints[0];
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
const closingDistance = Math.sqrt(
Math.pow(firstPoint.x - lastPoint.x, 2) +
Math.pow(firstPoint.y - lastPoint.y, 2)
Math.pow(firstPoint.x - lastPoint.x, 2) + Math.pow(firstPoint.y - lastPoint.y, 2)
);
// 如果首尾距离较大,自动添加闭合线段

View File

@@ -261,10 +261,7 @@ const actions = {
}
// 如果有当前笔刷实例且有更新方法,则调用
if (
state.currentBrushInstance &&
state.currentBrushInstance.updateProperty
) {
if (state.currentBrushInstance && state.currentBrushInstance.updateProperty) {
state.currentBrushInstance.updateProperty(propId, value);
}
},
@@ -277,7 +274,7 @@ const actions = {
*/
getPropertyValue(propId, defaultValue) {
// 检查属性值是否存在
if (state.propertyValues.hasOwnProperty(propId)) {
if (Object.prototype.hasOwnProperty.call(state.propertyValues, propId)) {
return state.propertyValues[propId];
}
@@ -551,10 +548,7 @@ const actions = {
// 导入材质预设
if (config.texturePresets && Array.isArray(config.texturePresets)) {
state.texturePresets = [
...state.texturePresets,
...config.texturePresets,
];
state.texturePresets = [...state.texturePresets, ...config.texturePresets];
}
// 导入自定义材质
@@ -580,9 +574,7 @@ const actions = {
state.texturePresets.forEach((preset, index) => {
const texture = texturePresetManager.getTextureById(preset.textureId);
if (!texture) {
console.warn(
`材质预设 "${preset.name}" 引用的材质 ${preset.textureId} 不存在`
);
console.warn(`材质预设 "${preset.name}" 引用的材质 ${preset.textureId} 不存在`);
// 可以选择使用默认材质替换或删除该预设
if (texturePresetManager.getAllTextures().length > 0) {
preset.textureId = texturePresetManager.getAllTextures()[0].id;
@@ -654,9 +646,7 @@ export const BrushStore = {
* @returns {Object} {r, g, b, a} 颜色值对象
*/
parseRGBAColor(rgbaColor) {
const rgbaMatch = rgbaColor.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
);
const rgbaMatch = rgbaColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (rgbaMatch) {
return {
r: parseInt(rgbaMatch[1]),

View File

@@ -113,23 +113,12 @@ async function testCPUManagerBasics() {
// 应用变形
const result = manager.applyDeformation(50, 50);
if (
!result ||
result.width !== testImageData.width ||
result.height !== testImageData.height
) {
if (!result || result.width !== testImageData.width || result.height !== testImageData.height) {
throw new Error("CPU管理器变形结果无效");
}
// 测试不同模式
const modes = [
"push",
"clockwise",
"counterclockwise",
"pinch",
"expand",
"reconstruct",
];
const modes = ["push", "clockwise", "counterclockwise", "pinch", "expand", "reconstruct"];
for (const mode of modes) {
manager.setMode(mode);
const modeResult = manager.applyDeformation(25, 25);
@@ -170,11 +159,7 @@ async function testWebGLManagerBasics() {
// 应用变形
const result = manager.applyDeformation(50, 50);
if (
!result ||
result.width !== testImageData.width ||
result.height !== testImageData.height
) {
if (!result || result.width !== testImageData.width || result.height !== testImageData.height) {
throw new Error("WebGL管理器变形结果无效");
}
} catch (error) {
@@ -300,11 +285,9 @@ async function testCoordinateConversion() {
// 转换到图像坐标
let imageX =
(localX + mockFabricObject.width / 2) *
(testImageData.width / mockFabricObject.width);
(localX + mockFabricObject.width / 2) * (testImageData.width / mockFabricObject.width);
let imageY =
(localY + mockFabricObject.height / 2) *
(testImageData.height / mockFabricObject.height);
(localY + mockFabricObject.height / 2) * (testImageData.height / mockFabricObject.height);
// 处理翻转
if (mockFabricObject.flipX) {
@@ -364,9 +347,7 @@ async function testPerformance() {
// 验证性能阈值每次操作不应超过50ms
if (avgTime > 50) {
throw new Error(
`性能不达标:平均操作时间 ${avgTime.toFixed(2)}ms 超过阈值 50ms`
);
throw new Error(`性能不达标:平均操作时间 ${avgTime.toFixed(2)}ms 超过阈值 50ms`);
}
}
@@ -374,9 +355,7 @@ async function testPerformance() {
* 测试内存管理
*/
async function testMemoryManagement() {
const initialMemory = performance.memory
? performance.memory.usedJSHeapSize
: 0;
const initialMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
// 创建多个管理器实例
const managers = [];
@@ -399,9 +378,7 @@ async function testMemoryManagement() {
window.gc();
}
const finalMemory = performance.memory
? performance.memory.usedJSHeapSize
: 0;
const finalMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
const memoryIncrease = finalMemory - initialMemory;
console.log(
@@ -414,9 +391,7 @@ async function testMemoryManagement() {
// 验证内存增长不超过10MB基本的内存泄漏检查
if (memoryIncrease > 10 * 1024 * 1024) {
console.warn(
`潜在内存泄漏: 内存增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
);
console.warn(`潜在内存泄漏: 内存增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
}
}
@@ -454,12 +429,7 @@ export async function runLiquifyIntegrationTests() {
console.log(`总测试数: ${testResults.totalTests}`);
console.log(`通过: ${testResults.passedTests}`);
console.log(`失败: ${testResults.failedTests}`);
console.log(
`成功率: ${(
(testResults.passedTests / testResults.totalTests) *
100
).toFixed(1)}%`
);
console.log(`成功率: ${((testResults.passedTests / testResults.totalTests) * 100).toFixed(1)}%`);
if (testResults.errors.length > 0) {
console.log("\n❌ 失败的测试:");

View File

@@ -45,19 +45,13 @@ export function testCoordinateConversion() {
// 模拟转换结果
const expectedImageX =
((coord.x - mockFabricObject.left) / mockFabricObject.scaleX +
mockFabricObject.width / 2) *
((coord.x - mockFabricObject.left) / mockFabricObject.scaleX + mockFabricObject.width / 2) *
(mockImageData.width / mockFabricObject.width);
const expectedImageY =
((coord.y - mockFabricObject.top) / mockFabricObject.scaleY +
mockFabricObject.height / 2) *
((coord.y - mockFabricObject.top) / mockFabricObject.scaleY + mockFabricObject.height / 2) *
(mockImageData.height / mockFabricObject.height);
console.log(
` 预期图像坐标: (${expectedImageX.toFixed(2)}, ${expectedImageY.toFixed(
2
)})`
);
console.log(` 预期图像坐标: (${expectedImageX.toFixed(2)}, ${expectedImageY.toFixed(2)})`);
});
console.log("坐标转换测试完成");
@@ -83,10 +77,7 @@ export function testPushAlgorithmPerformance() {
// 模拟液化计算
const movementLength =
i > 0
? Math.sqrt(
Math.pow(x - movements[i - 1].x, 2) +
Math.pow(y - movements[i - 1].y, 2)
)
? Math.sqrt(Math.pow(x - movements[i - 1].x, 2) + Math.pow(y - movements[i - 1].y, 2))
: 0;
if (movementLength > 0.5) {
@@ -151,12 +142,8 @@ export function testScaleConsistency() {
const localX = (canvasCoord.x - fabricObject.left) / fabricObject.scaleX;
const localY = (canvasCoord.y - fabricObject.top) / fabricObject.scaleY;
const imageX =
(localX + fabricObject.width / 2) *
(imageData.width / fabricObject.width);
const imageY =
(localY + fabricObject.height / 2) *
(imageData.height / fabricObject.height);
const imageX = (localX + fabricObject.width / 2) * (imageData.width / fabricObject.width);
const imageY = (localY + fabricObject.height / 2) * (imageData.height / fabricObject.height);
console.log(` 画布坐标: (${canvasCoord.x}, ${canvasCoord.y})`);
console.log(` 本地坐标: (${localX.toFixed(2)}, ${localY.toFixed(2)})`);
@@ -164,10 +151,7 @@ export function testScaleConsistency() {
// 验证图像坐标是否在合理范围内
const isValid =
imageX >= 0 &&
imageX < imageData.width &&
imageY >= 0 &&
imageY < imageData.height;
imageX >= 0 && imageX < imageData.width && imageY >= 0 && imageY < imageData.height;
console.log(` 坐标有效性: ${isValid ? "✓" : "✗"}`);
});
@@ -190,10 +174,7 @@ export function runAllTests() {
console.log("=== 液化功能测试完成 ===");
console.log(
`推荐配置: 节流时间 ${Math.max(
16,
perfResult.avgTimePerOperation * 2
).toFixed(0)}ms`
`推荐配置: 节流时间 ${Math.max(16, perfResult.avgTimePerOperation * 2).toFixed(0)}ms`
);
return {

View File

@@ -81,11 +81,7 @@ export class LayerSort {
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (!layer.isBackground && !layer.isFixed) {
// 普通图层
currentZIndex = this.processLayerObjects(
layer,
currentZIndex,
zIndexMap
);
currentZIndex = this.processLayerObjects(layer, currentZIndex, zIndexMap);
}
}
@@ -142,8 +138,7 @@ export class LayerSort {
getChildLayersInOrder(parentLayerId) {
// 获取所有子图层
const childLayers =
this.layers.value.filter((layer) => layer.id === parentLayerId)
?.children || [];
this.layers.value.filter((layer) => layer.id === parentLayerId)?.children || [];
return childLayers;
}
@@ -194,9 +189,7 @@ export class LayerSort {
for (let i = start; i < end; i++) {
const item = sortedObjects[i];
const currentIndex = this.canvas
.getObjects()
.indexOf(item.object);
const currentIndex = this.canvas.getObjects().indexOf(item.object);
if (currentIndex !== i && currentIndex !== -1) {
this.canvas.moveTo(item.object, i);
}
@@ -288,23 +281,17 @@ export class LayerSort {
return this.layers.value.length; // 背景图层插入到最后
} else if (newLayer.isFixed) {
// 固定图层插入到背景图层之前
const bgIndex = this.layers.value.findIndex(
(layer) => layer.isBackground
);
const bgIndex = this.layers.value.findIndex((layer) => layer.isBackground);
return bgIndex !== -1 ? bgIndex : this.layers.value.length;
} else {
// 普通图层插入到固定图层之前
const fixedIndex = this.layers.value.findIndex(
(layer) => layer.isFixed
);
const fixedIndex = this.layers.value.findIndex((layer) => layer.isFixed);
return fixedIndex !== -1 ? fixedIndex : this.layers.value.length;
}
}
// 如果指定了目标图层,插入到目标图层之前
const targetIndex = this.layers.value.findIndex(
(layer) => layer.id === targetLayerId
);
const targetIndex = this.layers.value.findIndex((layer) => layer.id === targetLayerId);
return targetIndex !== -1 ? targetIndex : this.layers.value.length;
}
@@ -532,9 +519,7 @@ export class LayerSort {
async smartSort(targetLayerIds = null) {
const layersToSort = targetLayerIds
? this.layers.value.filter((layer) => targetLayerIds.includes(layer.id))
: this.layers.value.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
: this.layers.value.filter((layer) => !layer.isBackground && !layer.isFixed);
if (layersToSort.length <= 1) return true;
@@ -556,9 +541,7 @@ export class LayerSort {
// 更新图层顺序
const sortedLayerIds = layersToSort.map((layer) => layer.id);
const otherLayers = this.layers.value.filter(
(layer) => !sortedLayerIds.includes(layer.id)
);
const otherLayers = this.layers.value.filter((layer) => !sortedLayerIds.includes(layer.id));
// 重新组织图层数组:保持背景层和固定层的位置
const newLayers = [];
@@ -733,12 +716,9 @@ export const LayerSortUtils = {
* @returns {number} 排序权重
*/
getLayerSortWeight(layer) {
if (layer.isBackground)
return LayerSortConstants.LAYER_PRIORITY[LayerType.BACKGROUND];
if (layer.isFixed)
return LayerSortConstants.LAYER_PRIORITY[LayerType.FIXED];
if (layer.children?.length > 0)
return LayerSortConstants.LAYER_PRIORITY[LayerType.GROUP];
if (layer.isBackground) return LayerSortConstants.LAYER_PRIORITY[LayerType.BACKGROUND];
if (layer.isFixed) return LayerSortConstants.LAYER_PRIORITY[LayerType.FIXED];
if (layer.children?.length > 0) return LayerSortConstants.LAYER_PRIORITY[LayerType.GROUP];
return LayerSortConstants.LAYER_PRIORITY[LayerType.NORMAL];
},

View File

@@ -9,7 +9,6 @@ export const createCanvas = (elementId, options = {}) => {
const canvas = new fabric.Canvas(elementId, {
enableRetinaScaling: canvasConfig.enableRetinaScaling,
renderOnAddRemove: false,
enableRetinaScaling: true,
preserveObjectStacking: true, // 保持对象堆叠顺序
// skipOffscreen: true, // 跳过离屏渲染
imageSmoothingEnabled: true, // 启用图像平滑 - 抗锯齿

View File

@@ -6,12 +6,7 @@ export function deepCompare(obj1, obj2) {
return null;
}
if (
obj1 === null ||
obj2 === null ||
typeof obj1 !== "object" ||
typeof obj2 !== "object"
) {
if (obj1 === null || obj2 === null || typeof obj1 !== "object" || typeof obj2 !== "object") {
return { _value: obj2, _oldValue: obj1 };
}
@@ -78,7 +73,7 @@ export function deepClone(obj) {
if (typeof obj === "object") {
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key]);
}
}
@@ -228,11 +223,7 @@ export function formatDuration(milliseconds) {
* @returns {boolean} 是否有效
*/
export function isValidCommand(command) {
return (
command &&
typeof command === "object" &&
typeof command.execute === "function"
);
return command && typeof command === "object" && typeof command.execute === "function";
}
/**
@@ -286,7 +277,7 @@ export function getObjectDepth(obj) {
let maxDepth = 0;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const depth = getObjectDepth(obj[key]);
maxDepth = Math.max(maxDepth, depth);
}
@@ -313,8 +304,7 @@ export function checkBrowserSupport() {
return {
WeakRef: typeof WeakRef !== "undefined",
FinalizationRegistry: typeof FinalizationRegistry !== "undefined",
PerformanceMemory:
typeof performance !== "undefined" && !!performance.memory,
PerformanceMemory: typeof performance !== "undefined" && !!performance.memory,
RequestIdleCallback: typeof requestIdleCallback !== "undefined",
IntersectionObserver: typeof IntersectionObserver !== "undefined",
ResizeObserver: typeof ResizeObserver !== "undefined",
@@ -337,12 +327,7 @@ export function delay(ms) {
* @returns {Promise} 执行结果
*/
export async function retry(fn, options = {}) {
const {
retries = 3,
delay: delayMs = 1000,
backoff = 1.5,
shouldRetry = () => true,
} = options;
const { retries = 3, delay: delayMs = 1000, backoff = 1.5, shouldRetry = () => true } = options;
let attempt = 0;
let currentDelay = delayMs;
@@ -377,9 +362,7 @@ export async function batchProcess(items, processor, options = {}) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => processor(item))
);
const batchResults = await Promise.all(batch.map((item) => processor(item)));
results.push(...batchResults);

View File

@@ -35,14 +35,8 @@ function initAligningGuidelines(canvas) {
ctx.lineWidth = aligningLineWidth;
ctx.strokeStyle = aligningLineColor;
ctx.beginPath();
ctx.moveTo(
x1 * zoom + viewportTransform[4],
y1 * zoom + viewportTransform[5]
);
ctx.lineTo(
x2 * zoom + viewportTransform[4],
y2 * zoom + viewportTransform[5]
);
ctx.moveTo(x1 * zoom + viewportTransform[4], y1 * zoom + viewportTransform[5]);
ctx.lineTo(x2 * zoom + viewportTransform[4], y2 * zoom + viewportTransform[5]);
ctx.stroke();
ctx.restore();
}
@@ -50,11 +44,7 @@ function initAligningGuidelines(canvas) {
function isInRange(value1, value2) {
value1 = Math.round(value1);
value2 = Math.round(value2);
for (
var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin;
i <= len;
i++
) {
for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) {
if (i === value2) {
return true;
}
@@ -77,8 +67,7 @@ function initAligningGuidelines(canvas) {
activeObjectLeft = activeObjectCenter.x,
activeObjectTop = activeObjectCenter.y,
activeObjectBoundingRect = activeObject.getBoundingRect(),
activeObjectHeight =
activeObjectBoundingRect.height / viewportTransform[3],
activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3],
activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0],
horizontalInTheRange = false,
verticalInTheRange = false,
@@ -100,12 +89,7 @@ function initAligningGuidelines(canvas) {
objectWidth = objectBoundingRect.width / viewportTransform[0];
// snaps if the right side of the active object touches the left side of the object
if (
isInRange(
activeObjectLeft + activeObjectWidth / 2,
objectLeft - objectWidth / 2
)
) {
if (isInRange(activeObjectLeft + activeObjectWidth / 2, objectLeft - objectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
@@ -120,22 +104,14 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft - objectWidth / 2 - activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
}
// snaps if the left side of the active object touches the right side of the object
if (
isInRange(
activeObjectLeft - activeObjectWidth / 2,
objectLeft + objectWidth / 2
)
) {
if (isInRange(activeObjectLeft - activeObjectWidth / 2, objectLeft + objectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
@@ -150,22 +126,14 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft + objectWidth / 2 + activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
}
// snaps if the bottom of the object touches the top of the active object
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
if (isInRange(objectTop + objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
@@ -180,22 +148,14 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 + activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 + activeObjectHeight / 2),
"center",
"center"
);
}
// snaps if the top of the object touches the bottom of the active object
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
if (isInRange(objectTop - objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
@@ -210,10 +170,7 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 - activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 - activeObjectHeight / 2),
"center",
"center"
);
@@ -241,12 +198,7 @@ function initAligningGuidelines(canvas) {
}
// snap by the left edge
if (
isInRange(
objectLeft - objectWidth / 2,
activeObjectLeft - activeObjectWidth / 2
)
) {
if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
@@ -260,22 +212,14 @@ function initAligningGuidelines(canvas) {
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
}
// snap by the right edge
if (
isInRange(
objectLeft + objectWidth / 2,
activeObjectLeft + activeObjectWidth / 2
)
) {
if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
@@ -289,10 +233,7 @@ function initAligningGuidelines(canvas) {
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
@@ -320,12 +261,7 @@ function initAligningGuidelines(canvas) {
}
// snap by the top edge
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
@@ -339,22 +275,14 @@ function initAligningGuidelines(canvas) {
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 + activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2),
"center",
"center"
);
}
// snap by the bottom edge
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
@@ -368,10 +296,7 @@ function initAligningGuidelines(canvas) {
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 - activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2),
"center",
"center"
);
@@ -397,7 +322,7 @@ function initAligningGuidelines(canvas) {
for (var i = verticalLines.length; i--; ) {
drawVerticalLine(verticalLines[i]);
}
for (var i = horizontalLines.length; i--; ) {
for (let i = horizontalLines.length; i--; ) {
drawHorizontalLine(horizontalLines[i]);
}
@@ -433,16 +358,14 @@ export function initCenteringGuidelines(canvas) {
viewportTransform;
for (
var i = canvasWidthCenter - centerLineMargin,
len = canvasWidthCenter + centerLineMargin;
var i = canvasWidthCenter - centerLineMargin, len = canvasWidthCenter + centerLineMargin;
i <= len;
i++
) {
canvasWidthCenterMap[Math.round(i)] = true;
}
for (
var i = canvasHeightCenter - centerLineMargin,
len = canvasHeightCenter + centerLineMargin;
let i = canvasHeightCenter - centerLineMargin, len = canvasHeightCenter + centerLineMargin;
i <= len;
i++
) {
@@ -450,21 +373,11 @@ export function initCenteringGuidelines(canvas) {
}
function showVerticalCenterLine() {
showCenterLine(
canvasWidthCenter + 0.5,
0,
canvasWidthCenter + 0.5,
canvasHeight
);
showCenterLine(canvasWidthCenter + 0.5, 0, canvasWidthCenter + 0.5, canvasHeight);
}
function showHorizontalCenterLine() {
showCenterLine(
0,
canvasHeightCenter + 0.5,
canvasWidth,
canvasHeightCenter + 0.5
);
showCenterLine(0, canvasHeightCenter + 0.5, canvasWidth, canvasHeightCenter + 0.5);
}
function showCenterLine(x1, y1, x2, y2) {
@@ -493,9 +406,8 @@ export function initCenteringGuidelines(canvas) {
if (!transform) return;
(isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap),
(isInHorizontalCenter =
Math.round(objectCenter.y) in canvasHeightCenterMap);
((isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap),
(isInHorizontalCenter = Math.round(objectCenter.y) in canvasHeightCenterMap));
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(

View File

@@ -1,13 +1,12 @@
/* eslint-disable no-async-promise-executor */
import { fabric } from "fabric-with-all";
import { LayerType, OperationType, createBitmapLayer } from "./layerHelper";
// 导入新的复合命令
import { CreateImageLayerCommand } from "../commands/LayerCommands";
// 导入新的命令
import {
ChangeFixedImageCommand,
AddImageToLayerCommand,
} from "../commands/LayerCommands";
import { ChangeFixedImageCommand, AddImageToLayerCommand } from "../commands/LayerCommands";
import { generateId } from "./helper";
import { isBoolean } from "lodash-es";
/**
* 加载并处理图片
@@ -101,9 +100,7 @@ export async function createImageLayer({
if (isBoolean(undoable)) createImageLayerCmd.undoable = undoable; // 是否撤销
// 执行复合命令
const newLayerId = await layerManager.commandManager.execute(
createImageLayerCmd
);
const newLayerId = await layerManager.commandManager.execute(createImageLayerCmd);
return newLayerId;
} catch (error) {
@@ -120,11 +117,7 @@ export async function createImageLayer({
* @param {Object} options.fabricImage - 新的图像对象
* @returns {Promise<boolean>} 是否成功更改
*/
export async function changeFixedImage({
layerManager,
fixedLayerId,
fabricImage,
} = {}) {
export async function changeFixedImage({ layerManager, fixedLayerId, fabricImage } = {}) {
if (!layerManager || !fixedLayerId || !fabricImage) {
console.error("更改固定图层图像:参数无效");
return false;
@@ -141,9 +134,7 @@ export async function changeFixedImage({
});
// 通过命令管理器执行
const result = await layerManager.commandManager.execute(
changeFixedImageCmd
);
const result = await layerManager.commandManager.execute(changeFixedImageCmd);
if (result) {
console.log(`✅ 成功更改固定图层 "${fixedLayerId}" 的图像`);
@@ -192,9 +183,7 @@ export async function addImageToLayer({
});
// 通过命令管理器执行
const resultLayerId = await layerManager.commandManager.execute(
addImageToLayerCmd
);
const resultLayerId = await layerManager.commandManager.execute(addImageToLayerCmd);
if (resultLayerId) {
if (targetLayerId) {
@@ -219,10 +208,7 @@ export async function addImageToLayer({
* @param {Object} options - 配置选项
* @returns {Promise<string>} 新图层ID的Promise
*/
export function loadImageUrlToLayer(
{ imageUrl, layerManager, canvas, toolManager },
options = {}
) {
export function loadImageUrlToLayer({ imageUrl, layerManager, canvas, toolManager }, options = {}) {
return new Promise(async (resolve, reject) => {
if (!imageUrl || !layerManager || !canvas) {
reject(new Error("参数无效"));
@@ -231,9 +217,7 @@ export function loadImageUrlToLayer(
try {
// 查找背景图层以获取尺寸
const bgLayer = layerManager.layers.value.find(
(layer) => layer.isBackground
);
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
// 设置最大宽高为背景图层的尺寸
const maxWidth = bgLayer?.canvasWidth || canvas.width;
@@ -304,9 +288,7 @@ export function uploadImageAndCreateLayer(
reader.onload = async (e) => {
try {
// 查找背景图层以获取尺寸
const bgLayer = layerManager.layers.value.find(
(layer) => layer.isBackground
);
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
// 设置最大宽高为背景图层的尺寸
const maxWidth = bgLayer?.canvasWidth || canvas.width;
@@ -361,9 +343,7 @@ export function safeLoadImage(imageSource, options = {}) {
.then(resolve)
.catch((error) => {
if (attempt < retries) {
console.warn(
`图片加载失败,正在重试 (${attempt + 1}/${retries})...`
);
console.warn(`图片加载失败,正在重试 (${attempt + 1}/${retries})...`);
setTimeout(() => attemptLoad(attempt + 1), 500);
} else {
reject(error);
@@ -426,12 +406,7 @@ export function loadImageAndChangeFixedLayer({
* @param {Object} options.imageOptions - 图片加载选项
* @returns {Promise<string>} 新图像对象ID的Promise
*/
export function uploadImageAndChangeFixedLayer({
file,
layerManager,
layerId,
imageOptions = {},
}) {
export function uploadImageAndChangeFixedLayer({ file, layerManager, layerId, imageOptions = {} }) {
return new Promise((resolve, reject) => {
if (!file || !layerManager || !layerId) {
reject(new Error("参数无效需要文件、图层管理器和图层ID"));
@@ -449,9 +424,7 @@ export function uploadImageAndChangeFixedLayer({
reader.onload = async (e) => {
try {
// 查找目标固定图层以获取尺寸信息
const targetLayer = layerManager.layers.value.find(
(layer) => layer.id === layerId
);
const targetLayer = layerManager.layers.value.find((layer) => layer.id === layerId);
if (!targetLayer) {
throw new Error(`找不到图层 ID: ${layerId}`);
@@ -463,9 +436,7 @@ export function uploadImageAndChangeFixedLayer({
}
// 查找背景图层以获取画布尺寸
const bgLayer = layerManager.layers.value.find(
(layer) => layer.isBackground
);
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
const maxWidth = bgLayer?.canvasWidth || layerManager.canvas.width;
const maxHeight = bgLayer?.canvasHeight || layerManager.canvas.height;
@@ -490,14 +461,10 @@ export function uploadImageAndChangeFixedLayer({
});
// 通过命令管理器执行
const newImageId = await layerManager.commandManager.execute(
changeFixedImageCmd
);
const newImageId = await layerManager.commandManager.execute(changeFixedImageCmd);
if (newImageId) {
console.log(
`✅ 成功更改固定图层 "${targetLayer.name}" 的图像新图像ID: ${newImageId}`
);
console.log(`✅ 成功更改固定图层 "${targetLayer.name}" 的图像新图像ID: ${newImageId}`);
resolve(newImageId);
} else {
throw new Error("更改固定图层图像失败");
@@ -826,9 +793,7 @@ export class AdvancedImageManager {
total: imageUrls.length,
successful: results.filter((r) => r.success).length,
failed: results.filter((r) => !r.success).length,
cacheHitRate:
this.performanceMetrics.cacheHits /
this.performanceMetrics.imageLoads,
cacheHitRate: this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads,
averageLoadTime: this.performanceMetrics.averageLoadTime,
},
};
@@ -884,13 +849,10 @@ export class AdvancedImageManager {
// 立即执行模式
for (const operation of operations) {
try {
const result = await this.canvasManager.changeFixedImage(
operation.imageUrl,
{
targetLayerType: operation.layerType,
...operation.options,
}
);
const result = await this.canvasManager.changeFixedImage(operation.imageUrl, {
targetLayerType: operation.layerType,
...operation.options,
});
operationResults.push({ success: true, ...result, operation });
} catch (error) {
operationResults.push({
@@ -1038,10 +1000,7 @@ export class AdvancedImageManager {
}
// 替换模板中的变量
const operations = this.replaceTemplateVariables(
template.operations,
variables
);
const operations = this.replaceTemplateVariables(template.operations, variables);
// 执行操作
const result = await this.batchAddImagesToLayers(operations);
@@ -1141,8 +1100,7 @@ export class AdvancedImageManager {
this.performanceMetrics.imageLoads++;
this.performanceMetrics.totalLoadTime += loadTime;
this.performanceMetrics.averageLoadTime =
this.performanceMetrics.totalLoadTime /
this.performanceMetrics.imageLoads;
this.performanceMetrics.totalLoadTime / this.performanceMetrics.imageLoads;
}
getSummary(results) {
@@ -1170,12 +1128,9 @@ export class AdvancedImageManager {
// 替换字符串中的变量 {{variable}}
Object.keys(newOp).forEach((key) => {
if (typeof newOp[key] === "string") {
newOp[key] = newOp[key].replace(
/\{\{(\w+)\}\}/g,
(match, varName) => {
return variables[varName] || match;
}
);
newOp[key] = newOp[key].replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] || match;
});
}
});
@@ -1223,10 +1178,7 @@ export class AdvancedImageManager {
generatePerformanceRecommendations() {
const recommendations = [];
if (
this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads <
0.3
) {
if (this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads < 0.3) {
recommendations.push("考虑增加缓存大小以提高缓存命中率");
}
@@ -1298,12 +1250,7 @@ export const ImageUtils = {
* @param {string} targetLayerId - 目标图层ID (可选)
* @returns {Promise<Object>} 执行结果
*/
quickAddImageToLayer: (
file,
layerManager,
toolManager,
targetLayerId = null
) => {
quickAddImageToLayer: (file, layerManager, toolManager, targetLayerId = null) => {
return uploadImageAndAddToLayerSimple({
file,
layerManager,
@@ -1376,12 +1323,7 @@ export function rasterizeCanvasObjects(options = {}) {
function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
return new Promise((resolve, reject) => {
try {
const {
trimWhitespace = true,
trimPadding = 10,
quality = 1,
format = "png",
} = options;
const { trimWhitespace = true, trimPadding = 10, quality = 1, format = "png" } = options;
// 保存原始状态
const originalObjects = canvas.getObjects();
@@ -1389,9 +1331,7 @@ function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
const originalZoom = canvas.getZoom();
// 临时隐藏其他对象,只显示要栅格化的对象
const objectsToHide = originalObjects.filter(
(obj) => !objects.includes(obj)
);
const objectsToHide = originalObjects.filter((obj) => !objects.includes(obj));
// 隐藏不需要的对象
objectsToHide.forEach((obj) => {
@@ -1415,9 +1355,7 @@ function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
const pixelRatio = canvas.getRetinaScaling();
// 复制画布元素(这会保持所有变换状态)
const copiedCanvas = fabric.util.copyCanvasElement(
canvas.lowerCanvasEl
);
const copiedCanvas = fabric.util.copyCanvasElement(canvas.lowerCanvasEl);
let finalCanvas = copiedCanvas;
let trimOffset = { x: 0, y: 0 };
@@ -1496,7 +1434,7 @@ function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
*/
function _restoreObjectVisibility(objects) {
objects.forEach((obj) => {
if (obj.hasOwnProperty("_originalVisible")) {
if (Object.prototype.hasOwnProperty.call(obj, "_originalVisible")) {
obj.set("visible", obj._originalVisible);
delete obj._originalVisible;
}
@@ -1521,9 +1459,7 @@ function _rasterizeUsingDataURL(canvas, objects, options = {}) {
const originalObjects = canvas.getObjects();
// 临时移除其他对象
const objectsToRemove = originalObjects.filter(
(obj) => !objects.includes(obj)
);
const objectsToRemove = originalObjects.filter((obj) => !objects.includes(obj));
objectsToRemove.forEach((obj) => {
canvas.remove(obj);
});
@@ -1770,12 +1706,7 @@ function _trimCanvas(canvas, padding = 0) {
trimmedCanvas.height = trimHeight;
// 复制裁剪区域包含padding
const trimmedImageData = ctx.getImageData(
paddedMinX,
paddedMinY,
trimWidth,
trimHeight
);
const trimmedImageData = ctx.getImageData(paddedMinX, paddedMinY, trimWidth, trimHeight);
trimmedCtx.putImageData(trimmedImageData, 0, 0);
return {
@@ -1955,8 +1886,7 @@ export function smartRasterize(options = {}) {
function _analyzeRasterizationContext(canvas, objects) {
const zoom = canvas.getZoom();
const viewportTransform = canvas.viewportTransform;
const hasTransform =
zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
const hasTransform = zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
// 分析对象类型分布
const objectTypes = objects.reduce((acc, obj) => {
@@ -1971,9 +1901,7 @@ function _analyzeRasterizationContext(canvas, objects) {
// 计算画布利用率
const canvasArea = canvas.width * canvas.height;
const objectsBounds = _calculateObjectsBounds(objects);
const objectsArea = objectsBounds
? objectsBounds.width * objectsBounds.height
: 0;
const objectsArea = objectsBounds ? objectsBounds.width * objectsBounds.height : 0;
const utilization = objectsArea / canvasArea;
return {
@@ -1985,9 +1913,7 @@ function _analyzeRasterizationContext(canvas, objects) {
utilization,
canvasSize: { width: canvas.width, height: canvas.height },
objectsBounds,
supportsCanvasCopy: !!(
fabric.util.copyCanvasElement && canvas.lowerCanvasEl
),
supportsCanvasCopy: !!(fabric.util.copyCanvasElement && canvas.lowerCanvasEl),
};
}
@@ -2223,12 +2149,7 @@ function _getAlternativeMethods(analysis) {
* @param {number} params.canvasWidth - 画布宽度
* @param {number} params.canvasHeight - 画布高度
*/
export const imageModeHandler = ({
imageMode,
newImage,
canvasWidth,
canvasHeight,
}) => {
export const imageModeHandler = ({ imageMode, newImage, canvasWidth, canvasHeight }) => {
switch (imageMode) {
case "stretch":
// 拉伸模式 - 填充整个画布
@@ -2253,10 +2174,8 @@ export const imageModeHandler = ({
// 这里可以添加裁剪逻辑,如果需要的话
// 例如使用fabric.Image.clipPath来裁剪图像
break;
case "contains":
// 包含模式 - 保证图像在画布内完整显示
// 既要考虑画布的宽高比,也要考虑图像的宽高比
// 图片缩放后要保证最长边能完全显示在画布内
case "contains": {
// 图片缩放后要保证最长边能完全显示在画布内 // 既要考虑画布的宽高比,也要考虑图像的宽高比 // 包含模式 - 保证图像在画布内完整显示
const canvasAspect = canvasWidth / canvasHeight;
const imageAspect = newImage.width / newImage.height;
// 保证图像在画布内完整显示 - 既要考虑画布的宽高比,也要考虑图像的宽高比
@@ -2269,5 +2188,6 @@ export const imageModeHandler = ({
newImage.scaleToHeight(canvasHeight);
}
break;
}
}
};

View File

@@ -95,8 +95,7 @@ export const BlendMode = {
export function isGroupLayer(layer) {
if (!layer) return false;
return (
layer.type === LayerType.GROUP ||
(Array.isArray(layer.children) && layer.children.length > 0)
layer.type === LayerType.GROUP || (Array.isArray(layer.children) && layer.children.length > 0)
);
}
@@ -107,11 +106,7 @@ export function isGroupLayer(layer) {
* @param {Object} options 其他选项
* @returns {Object} 创建的图层对象
*/
export function createLayerFromFabricObject(
fabricObject,
layerType = "bitmap",
options = {}
) {
export function createLayerFromFabricObject(fabricObject, layerType = "bitmap", options = {}) {
if (!fabricObject) return null;
// 确定图层类型
@@ -137,9 +132,7 @@ export function createLayerFromFabricObject(
type: type,
name:
options.name ||
`${
fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1)
} 图层`,
`${fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1)} 图层`,
parentId: options.parentId || null,
});
@@ -166,9 +159,7 @@ export function createLayerFromFabricObject(
*/
export function createLayer(options = {}) {
const id =
options.id ||
generateId("layer_") ||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
options.id || generateId("layer_") || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
@@ -208,8 +199,7 @@ export function createLayer(options = {}) {
* @returns {Object} 背景图层对象
*/
export function createBackgroundLayer(options = {}) {
const id =
options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const id = options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
@@ -400,9 +390,7 @@ export function createSmartObjectLayer(options = {}) {
* @returns {Object} 固定图层对象
*/
export function createFixedLayer(options = {}) {
const id =
options.id ||
`fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const id = options.id || `fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
@@ -445,9 +433,7 @@ export function cloneLayer(layer) {
// 复制 fabric 对象 (如果存在)
if (Array.isArray(layer.fabricObjects)) {
clonedLayer.fabricObjects = layer.fabricObjects.map((obj) => {
return obj && typeof obj.clone === "function"
? obj.clone()
: JSON.parse(JSON.stringify(obj));
return obj && typeof obj.clone === "function" ? obj.clone() : JSON.parse(JSON.stringify(obj));
});
}

View File

@@ -15,18 +15,13 @@ export function buildLayerAssociations(layer, canvasObjects) {
// 处理单个fabricObject关联
if (layer.fabricObject) {
// 如果图层已经有关联的fabricObject确保它的layerId和layerName正确
layer.fabricObject =
canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
layer.fabricObject = canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
}
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组 也可能是一个简单对象
const clippingMaskObj = canvasObjects.find(
(obj) => obj.id === layer.clippingMask.id
);
layer.clippingMask = clippingMaskObj
? clippingMaskObj?.toObject?.(["id"])
: layer.clippingMask;
const clippingMaskObj = canvasObjects.find((obj) => obj.id === layer.clippingMask.id);
layer.clippingMask = clippingMaskObj ? clippingMaskObj?.toObject?.(["id"]) : layer.clippingMask;
}
// 处理多个fabricObjects关联
@@ -172,10 +167,7 @@ export function simplifyLayers(layers) {
opacity: layer.opacity,
isBackground: layer.isBackground || false,
isFixed: layer.isFixed || false,
clippingMask:
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
layer.clippingMask ||
null, // 可能是一个fabricObject或组 也可能是一个简单对象
clippingMask: layer.clippingMask?.toObject?.(["id", "layerId"]) || layer.clippingMask || null, // 可能是一个fabricObject或组 也可能是一个简单对象
// ? {
// id: layer.clippingMask.id,
// type: layer.clippingMask.type,
@@ -200,10 +192,7 @@ export function simplifyLayers(layers) {
)
.filter((obj) => obj !== null)
: [],
children:
layer.children && isArray(layer.children)
? simplifyLayers(layer.children)
: [],
children: layer.children && isArray(layer.children) ? simplifyLayers(layer.children) : [],
};
return simplifiedLayer;
@@ -240,9 +229,7 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组 也可能是一个简单对象
const clippingMaskObj = canvasObjects.find(
(obj) => obj.id === layer.clippingMask.id
);
const clippingMaskObj = canvasObjects.find((obj) => obj.id === layer.clippingMask.id);
restoredLayer.clippingMask = clippingMaskObj
? clippingMaskObj?.toObject?.(["id"])
: layer.clippingMask;
@@ -250,17 +237,11 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
// 恢复单个fabricObject关联
if (layer.fabricObject?.id) {
const fabricObj = canvasObjects.find(
(obj) => obj.id === layer.fabricObject.id
);
const fabricObj = canvasObjects.find((obj) => obj.id === layer.fabricObject.id);
if (fabricObj) {
fabricObj.layerId = layer.id;
fabricObj.layerName = layer.name;
restoredLayer.fabricObject = fabricObj.toObject([
"id",
"layerId",
"type",
]);
restoredLayer.fabricObject = fabricObj.toObject(["id", "layerId", "type"]);
} else {
restoredLayer.fabricObject = null;
}
@@ -270,9 +251,7 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
restoredLayer.fabricObjects = layer.fabricObjects
.map((fabricRef) => {
const fabricObj = canvasObjects.find(
(obj) => obj.id === fabricRef.id
);
const fabricObj = canvasObjects.find((obj) => obj.id === fabricRef.id);
if (fabricObj) {
fabricObj.layerId = layer.id;
fabricObj.layerName = layer.name;

View File

@@ -18,10 +18,8 @@ export async function restoreFabricObject(serializedObject, canvas) {
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId)
fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName)
fabricObject.layerName = serializedObject.layerName;
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
// 更新坐标
fabricObject.setCoords();

View File

@@ -22,11 +22,7 @@ export const createRasterizedImage = async ({
isGroupWithMask = false, // 是否为带遮罩的组图层
} = {}) => {
try {
console.log(
`📊 开始栅格化 ${fabricObjects.length} 个对象${
maskObject ? "(带遮罩)" : ""
}`
);
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象${maskObject ? "(带遮罩)" : ""}`);
// 确保有对象需要栅格化
if (fabricObjects.length === 0) {
@@ -36,10 +32,7 @@ export const createRasterizedImage = async ({
// 高清倍数
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom);
if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数
@@ -191,9 +184,7 @@ const createRasterizedImageWithGroup = async ({
hasControls: false,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 调整组的位置,让它位于画布的左上角
group.set({
@@ -317,9 +308,7 @@ const createRasterizedImageWithMask = async ({
height: canvasHeight,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 调整对象位置,相对于遮罩边界重新定位
clonedObjects.forEach((obj) => {

View File

@@ -85,18 +85,12 @@ const createClippedObjects = async ({
console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
const optimizedBounds = calculateOptimizedBounds(clippingObject, fabricObjects);
console.log("📐 优化后的选区边界框:", optimizedBounds);
// 获取羽化值
let featherAmount = 0;
if (
selectionManager &&
typeof selectionManager.getFeatherAmount === "function"
) {
if (selectionManager && typeof selectionManager.getFeatherAmount === "function") {
featherAmount = selectionManager.getFeatherAmount();
console.log(`🌟 应用羽化效果: ${featherAmount}px`);
}
@@ -177,10 +171,7 @@ const createClippedDataURLByCanvas = async ({
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
const optimizedBounds = calculateOptimizedBounds(clippingObject, fabricObjects);
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
@@ -239,13 +230,7 @@ const createClippedDataURLByCanvas = async ({
* 创建简单克隆对象
* 当不需要裁剪时,直接克隆原对象
*/
const createSimpleClone = async ({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
}) => {
const createSimpleClone = async ({ canvas, fabricObjects, isReturenDataURL, quality, format }) => {
try {
console.log("📋 创建简单克隆对象");
@@ -459,10 +444,7 @@ const createLegacyRasterization = async ({
// 这里保留原有的离屏渲染逻辑作为备选方案
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom);
scaleFactor = Math.min(scaleFactor, 3);
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
@@ -592,9 +574,7 @@ const createOffscreenRasterization = async ({
height: canvasHeight,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 克隆对象到离屏画布
const clonedObjects = [];
@@ -749,11 +729,7 @@ export const getObjectsBounds = (fabricObjects) => {
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 遮罩图像的DataURL
*/
const createMaskImageFromPath = async ({
clippingObject,
selectionBounds,
qualityMultiplier,
}) => {
const createMaskImageFromPath = async ({ clippingObject, selectionBounds, qualityMultiplier }) => {
try {
console.log("🎭 创建路径遮罩图像");
@@ -769,11 +745,7 @@ const createMaskImageFromPath = async ({
});
// 克隆路径对象并处理描边转填充
const maskPath = await createSolidMaskPath(
clippingObject,
selectionBounds,
qualityMultiplier
);
const maskPath = await createSolidMaskPath(clippingObject, selectionBounds, qualityMultiplier);
// 添加路径到遮罩画布
maskCanvas.add(maskPath);
@@ -804,11 +776,7 @@ const createMaskImageFromPath = async ({
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 内容图像的DataURL
*/
const renderContentToImage = async ({
fabricObjects,
selectionBounds,
qualityMultiplier,
}) => {
const renderContentToImage = async ({ fabricObjects, selectionBounds, qualityMultiplier }) => {
try {
console.log("🖼️ 渲染内容图像");
@@ -964,11 +932,7 @@ const createAdvancedMaskImage = async ({
});
// 克隆路径对象并处理描边转填充
const maskPath = await createSolidMaskPath(
clippingObject,
selectionBounds,
qualityMultiplier
);
const maskPath = await createSolidMaskPath(clippingObject, selectionBounds, qualityMultiplier);
// 如果有羽化值,添加模糊效果
if (featherAmount > 0) {
@@ -987,10 +951,7 @@ const createAdvancedMaskImage = async ({
// 如果有羽化,需要进行后处理
if (featherAmount > 0) {
return await applyCanvasBlur(
maskCanvas,
featherAmount * qualityMultiplier
);
return await applyCanvasBlur(maskCanvas, featherAmount * qualityMultiplier);
}
// 生成遮罩图像
@@ -1066,11 +1027,7 @@ const applyCanvasBlur = async (canvas, blurAmount) => {
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<fabric.Object>} 处理后的遮罩路径对象
*/
const createSolidMaskPath = async (
clippingObject,
selectionBounds,
qualityMultiplier
) => {
const createSolidMaskPath = async (clippingObject, selectionBounds, qualityMultiplier) => {
try {
console.log("🔧 创建实体遮罩路径,处理描边转填充");
@@ -1081,29 +1038,19 @@ const createSolidMaskPath = async (
const hasStroke = maskPath.stroke && maskPath.strokeWidth > 0;
if (hasStroke) {
console.log(
`📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`
);
console.log(`📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`);
// 对于有描边的路径,我们需要更精确的处理
const strokeWidth = maskPath.strokeWidth;
// 方法1: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
if (
maskPath.type === "rect" ||
maskPath.type === "circle" ||
maskPath.type === "ellipse"
) {
if (maskPath.type === "rect" || maskPath.type === "circle" || maskPath.type === "ellipse") {
// 对于矩形和椭圆,增加宽高来包含描边
const strokeOffset = strokeWidth;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset / 2) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset / 2) *
qualityMultiplier,
left: (maskPath.left - selectionBounds.left - strokeOffset / 2) * qualityMultiplier,
top: (maskPath.top - selectionBounds.top - strokeOffset / 2) * qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
width: (maskPath.width || 0) + strokeOffset,
@@ -1122,12 +1069,8 @@ const createSolidMaskPath = async (
const strokeOffset = strokeWidth / 2;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset) *
qualityMultiplier,
left: (maskPath.left - selectionBounds.left - strokeOffset) * qualityMultiplier,
top: (maskPath.top - selectionBounds.top - strokeOffset) * qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier * expandRatio,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier * expandRatio,
fill: "#ffffff",