diff --git a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js index b6ff2272..66880184 100644 --- a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js @@ -2058,7 +2058,7 @@ export class LayerObjectsToGroupCommand extends Command { } try { - await optimizeCanvasRendering(this.canvas, () => { + await optimizeCanvasRendering(this.canvas, async () => { if (existingGroup) { // 向现有组添加对象 this._addObjectsToExistingGroup(existingGroup, newObjectsToAdd); @@ -2069,8 +2069,8 @@ export class LayerObjectsToGroupCommand extends Command { this._createNewGroupWithAllObjects(newObjectsToAdd); this.groupObjectId = this.newGroupId; this.wasGroupCreated = true; + await this.layerManager?.layerSort?.rearrangeObjects(); } - // 更新交互性 this.layerManager?.updateLayersObjectsInteractivity?.(false).then(()=>{ // 更新缩略图 diff --git a/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js b/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js index d6106c8b..2ecda7b1 100644 --- a/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/LiquifyCommands.js @@ -1,5 +1,7 @@ import { Command, FunctionCommand } from "./Command"; import { getLiquifyReferenceManager } from "../managers/LiquifyReferenceManager"; +import { optimizeCanvasRendering } from "../utils/helper"; +import { fabric } from "fabric-with-all"; /** * 液化命令基类 @@ -59,9 +61,7 @@ export class LiquifyCommand extends Command { // 更新画布上的对象 await this._updateObjectWithResult(); - - // 刷新Canvas - this.canvas.renderAll(); + // 注意:_updateObjectWithResult 内部已使用 optimizeCanvasRendering,会自动渲染 return this.resultData; } @@ -130,6 +130,7 @@ export class LiquifyCommand extends Command { /** * 使用变形结果更新对象 + * 优化:直接使用 setElement 更新现有对象,避免创建新对象和替换操作 * @private */ async _updateObjectWithResult() { @@ -142,48 +143,103 @@ export class LiquifyCommand extends Command { const tempCtx = tempCanvas.getContext("2d"); tempCtx.putImageData(this.resultData, 0, 0); console.log("临时Canvas创建成功 _updateObjectWithResult", this.resultData); - // 更新Fabric图像 - await new Promise((resolve) => { - fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => { - // 保留原对象的属性 - img.set({ - left: this.targetObject.left, - top: this.targetObject.top, - scaleX: this.targetObject.scaleX, - scaleY: this.targetObject.scaleY, - angle: this.targetObject.angle, - flipX: this.targetObject.flipX, - flipY: this.targetObject.flipY, - opacity: this.targetObject.opacity, - }); - - // 替换Canvas上的对象 - const index = this.canvas.getObjects().indexOf(this.targetObject); - if (index !== -1) { - this.canvas.remove(this.targetObject); - this.canvas.insertAt(img, index); - this.targetObject = img; - } - - // 确保图层引用更新 - const layer = this.layerManager.getLayerById(this.targetLayerId); - if (layer) { - if (layer.type === "background" && layer.fabricObject === this.targetObject) { - layer.fabricObject = img; - } else if (layer.fabricObjects) { - const objIndex = layer.fabricObjects.indexOf(this.targetObject); - if (objIndex !== -1) { - layer.fabricObjects[objIndex] = img; - } - } - } - - resolve(); - }); + + // 使用优化渲染工具,避免图层闪烁 + await optimizeCanvasRendering(this.canvas, async () => { + // 预加载图像元素,确保完全加载后再更新 + await this._updateObjectElementDirectly(tempCanvas.toDataURL()); }); return true; } + + /** + * 直接更新对象的图像元素,避免对象替换造成的图层闪烁 + * @param {String} imageDataURL 图像数据的 DataURL + * @private + */ + async _updateObjectElementDirectly(imageDataURL) { + return new Promise((resolve, reject) => { + // 创建 HTMLImageElement 并预加载 + const imgElement = new Image(); + + imgElement.onload = () => { + try { + // 保存当前对象状态(非图像属性) + const currentState = { + left: this.targetObject.left, + top: this.targetObject.top, + scaleX: this.targetObject.scaleX, + scaleY: this.targetObject.scaleY, + angle: this.targetObject.angle, + flipX: this.targetObject.flipX, + flipY: this.targetObject.flipY, + opacity: this.targetObject.opacity, + }; + + // 直接更新现有对象的图像元素,保持对象引用不变 + if (this.targetObject.setElement) { + this.targetObject.setElement(imgElement); + } else if (this.targetObject._element) { + this.targetObject._element = imgElement; + this.targetObject._originalElement = imgElement; + } + + // 恢复对象属性(setElement 可能会重置一些属性) + this.targetObject.set(currentState); + + // 标记对象需要重新渲染 + this.targetObject.dirty = true; + this.targetObject.setCoords(); + + resolve(); + } catch (error) { + console.error("更新对象元素失败:", error); + reject(error); + } + }; + + imgElement.onerror = () => { + reject(new Error("图像加载失败")); + }; + + // 开始加载图像 + imgElement.src = imageDataURL; + }); + } + + /** + * 安全地替换对象,避免图层闪烁(保留作为备用方法) + * @param {Object} newImg 新的fabric图像对象 + * @private + */ + _replaceObjectSafely(newImg) { + // 保存旧对象引用,用于更新图层引用 + const oldObject = this.targetObject; + + // 替换Canvas上的对象 + const index = this.canvas.getObjects().indexOf(oldObject); + if (index !== -1) { + // 在禁用自动渲染的情况下,先插入新对象,再移除旧对象 + // 这样可以避免中间出现空白(因为renderOnAddRemove已被禁用) + this.canvas.insertAt(newImg, index); + this.canvas.remove(oldObject); + this.targetObject = newImg; + } + + // 确保图层引用更新 + const layer = this.layerManager.getLayerById(this.targetLayerId); + if (layer) { + if (layer.type === "background" && layer.fabricObject === oldObject) { + layer.fabricObject = newImg; + } else if (layer.fabricObjects) { + const objIndex = layer.fabricObjects.indexOf(oldObject); + if (objIndex !== -1) { + layer.fabricObjects[objIndex] = newImg; + } + } + } + } } /** @@ -271,8 +327,8 @@ export class LiquifyDeformCommand extends LiquifyCommand { // 应用变形结果 await this._updateObjectWithImageData(this.afterData); + // 注意:_updateObjectWithImageData 内部已使用 optimizeCanvasRendering,会自动渲染 - this.canvas.renderAll(); return this.afterData; } @@ -283,70 +339,70 @@ export class LiquifyDeformCommand extends LiquifyCommand { // 恢复到变形前的状态 await this._updateObjectWithImageData(this.beforeData); + // 注意:_updateObjectWithImageData 内部已使用 optimizeCanvasRendering,会自动渲染 - this.canvas.renderAll(); return true; } /** * 使用图像数据更新对象 + * 优化:直接使用 setElement 更新现有对象,避免创建新对象和替换操作 * @param {ImageData} imageData 图像数据 * @private */ async _updateObjectWithImageData(imageData) { - return new Promise((resolve) => { - // 创建临时canvas - const tempCanvas = document.createElement("canvas"); - tempCanvas.width = imageData.width; - tempCanvas.height = imageData.height; - const tempCtx = tempCanvas.getContext("2d"); - tempCtx.putImageData(imageData, 0, 0); + // 创建临时canvas + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = imageData.width; + tempCanvas.height = imageData.height; + const tempCtx = tempCanvas.getContext("2d"); + tempCtx.putImageData(imageData, 0, 0); - // 从canvas创建新的fabric图像 - fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => { - // 保留原对象的变换属性 - img.set({ - left: this.targetObject.left, - top: this.targetObject.top, - scaleX: this.targetObject.scaleX, - scaleY: this.targetObject.scaleY, - angle: this.targetObject.angle, - flipX: this.targetObject.flipX, - flipY: this.targetObject.flipY, - opacity: this.targetObject.opacity, - }); - - // 替换canvas上的对象 - const index = this.canvas.getObjects().indexOf(this.targetObject); - if (index !== -1) { - this.canvas.remove(this.targetObject); - this.canvas.insertAt(img, index); - this.targetObject = img; - - // 更新图层引用 - const layer = this.layerManager.getLayerById(this.targetLayerId); - if (layer) { - if ( - layer.type === "background" && - (layer.fabricObject === this.savedState?.originalObject || - layer.fabricObject === this.targetObject) - ) { - layer.fabricObject = img; - } else if (layer.fabricObjects) { - const objIndex = layer.fabricObjects.findIndex( - (obj) => obj === this.savedState?.originalObject || obj === this.targetObject - ); - if (objIndex !== -1) { - layer.fabricObjects[objIndex] = img; - } - } - } - } - - resolve(); - }); + // 使用优化渲染工具,避免图层闪烁 + await optimizeCanvasRendering(this.canvas, async () => { + // 直接更新对象元素,避免对象替换 + await this._updateObjectElementDirectly(tempCanvas.toDataURL()); }); } + + /** + * 安全地替换变形对象,避免图层闪烁(保留作为备用方法) + * @param {Object} newImg 新的fabric图像对象 + * @private + */ + _replaceDeformObjectSafely(newImg) { + // 保存旧对象引用,用于更新图层引用 + const oldObject = this.targetObject; + + // 替换Canvas上的对象 + const index = this.canvas.getObjects().indexOf(oldObject); + if (index !== -1) { + // 在禁用自动渲染的情况下,先插入新对象,再移除旧对象 + // 这样可以避免中间出现空白 + this.canvas.insertAt(newImg, index); + this.canvas.remove(oldObject); + this.targetObject = newImg; + } + + // 更新图层引用 + const layer = this.layerManager.getLayerById(this.targetLayerId); + if (layer) { + if ( + layer.type === "background" && + (layer.fabricObject === this.savedState?.originalObject || + layer.fabricObject === oldObject) + ) { + layer.fabricObject = newImg; + } else if (layer.fabricObjects) { + const objIndex = layer.fabricObjects.findIndex( + (obj) => obj === this.savedState?.originalObject || obj === oldObject + ); + if (objIndex !== -1) { + layer.fabricObjects[objIndex] = newImg; + } + } + } + } } /** diff --git a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js index d9b60f4a..1243295a 100644 --- a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js @@ -255,6 +255,8 @@ export class AddObjectToLayerCommand extends Command { ); // 标记为非首次执行 this.isFirstExecution = false; + // 重新排序图层对象 + await this.layerManager?.layerSort?.rearrangeObjects(); console.log( `✅ 对象已添加到图层 "${layer.name}",位置: (${this.fabricObject.left}, ${this.fabricObject.top})` ); diff --git a/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js b/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js index 6b180729..7088029a 100644 --- a/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js +++ b/src/component/Canvas/CanvasEditor/commands/RasterizeLayerCommand.js @@ -359,7 +359,8 @@ export class RasterizeLayerCommand extends Command { // 设置为活动图层 this.activeLayerId.value = this.rasterizedLayerId; - + // 重新排序图层对象 + await this.layerManager?.layerSort?.rearrangeObjects(); await this.layerManager?.updateLayersObjectsInteractivity(false); console.log(`🎨 组合图层 ${this.rasterizedLayer.name} 创建完成`); diff --git a/src/component/Canvas/CanvasEditor/commands/TextCommands.js b/src/component/Canvas/CanvasEditor/commands/TextCommands.js index 4b02f510..c9c464ef 100644 --- a/src/component/Canvas/CanvasEditor/commands/TextCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/TextCommands.js @@ -409,6 +409,9 @@ export class CreateTextCommand extends Command { // 现在可以安全地设置为活动图层 this.layerManager.setActiveLayer(this.layerId); + // 重新排序图层对象 + await this.layerManager?.layerSort?.rearrangeObjects(); + // 更新对象交互性 await this.layerManager?.updateLayersObjectsInteractivity?.(false); diff --git a/src/component/Canvas/CanvasEditor/components/LayersPanel/LayersPanel.vue b/src/component/Canvas/CanvasEditor/components/LayersPanel/LayersPanel.vue index 5d05111c..b6971bb3 100644 --- a/src/component/Canvas/CanvasEditor/components/LayersPanel/LayersPanel.vue +++ b/src/component/Canvas/CanvasEditor/components/LayersPanel/LayersPanel.vue @@ -589,11 +589,11 @@ function handleLayerClick(layer, event) { layerManager?.setAllActiveGroupLayerCanvasObject?.(layer); setActiveLayer(layer.children[0].id, { parentId: layer.id }); } else { + // 选中画布中的图层对象 + layerManager?.selectLayerObjects(layer.id); // 否则直接设置当前图层为活动图层 setActiveLayer(layer.id); layerManager?.updateLayersObjectsInteractivity(); - // 选中画布中的图层对象 - layerManager?.selectLayerObjects(layer.id); } } lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id); diff --git a/src/component/Canvas/CanvasEditor/components/SelectMenuPanel.vue b/src/component/Canvas/CanvasEditor/components/SelectMenuPanel.vue new file mode 100644 index 00000000..a340bde0 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/components/SelectMenuPanel.vue @@ -0,0 +1,609 @@ + + + + + diff --git a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue index f944678a..8b0596b7 100644 --- a/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue +++ b/src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue @@ -327,11 +327,14 @@ function handleKeyDown(event) { } } +const fillInputTimeout = ref(null); // 填充颜色选择器 function fillColorChange() { - layerManager.fillLayerBackground(lastSelectLayerId.value, fillColor.value, true); + clearTimeout(fillInputTimeout.value); + fillInputTimeout.value = setTimeout(() => { + layerManager.fillLayerBackground(lastSelectLayerId.value, fillColor.value, true); + }, 100); } - onMounted(() => { // 添加键盘事件监听 window.addEventListener("keydown", handleKeyDown); @@ -355,7 +358,7 @@ const handleToolClick = (tool) => { v-model="fillColor" ref="fillColorRef" type="color" - @change="fillColorChange" + @input="fillColorChange" style="width: 0; height: 0; opacity: 0" />
diff --git a/src/component/Canvas/CanvasEditor/fabric-canvas-events.text b/src/component/Canvas/CanvasEditor/fabric-canvas-events.text new file mode 100644 index 00000000..9d2692c9 --- /dev/null +++ b/src/component/Canvas/CanvasEditor/fabric-canvas-events.text @@ -0,0 +1,23 @@ +1. 初始化事件 +object:added:当新对象被添加到画布上时触发。 +object:removed:当对象从画布上移除时触发。 +selection:created:当选择对象时触发。 +selection:updated:当选择的对象被更新时触发。 +selection:cleared:当所有对象都被取消选择时触发。 + +2. 鼠标事件 +mouse:down:鼠标按下时触发。 +mouse:move:鼠标移动时触发。 +mouse:up:鼠标释放时触发。 +mouse:over:鼠标移到画布上时触发。 +mouse:out:鼠标移出画布时触发。 +mouse:wheel:鼠标滚轮滚动时触发。 + +3. 触摸事件(在触摸屏设备上) +touch:start:触摸开始时触发。 +touch:move:触摸移动时触发。 +touch:end:触摸结束时触发。 + +4. 键盘事件 +key:down:键盘按键按下时触发。 +key:up:键盘按键释放时触发。 \ No newline at end of file diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index 084b8908..1f44abc7 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -34,6 +34,7 @@ import LayersPanel from "./components/LayersPanel/LayersPanel.vue"; import BrushControlPanel from "./components/BrushControlPanel.vue"; import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板 import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板 +import SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件 import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板 import { LayerType, OperationType } from "./utils/layerHelper.js"; import { ToolManager } from "./managers/ToolManager.js"; @@ -619,12 +620,22 @@ function updateCanvasSize() { function updateCanvasColor() { canvasManager.setCanvasColor(canvasColor.value); } - +function createLayerName(){ + const layer = t("Canvas.layer") + // 检查图层名称是否已存在 + let layerIndex = 1; + let layerName = `${layer + " " + layerIndex}`; + while (layerManager.getLayerByName(layerName)) { + layerIndex++; + layerName = `${layer} ${layerIndex}`; + } + return layerName; +} async function addLayer() { - await layerManager.createLayer(t("Canvas.EmptyLayer")); + await layerManager.createLayer(createLayerName()); } async function addTopLayer() { - await layerManager.createLayer(t("Canvas.EmptyLayer"), LayerType.EMPTY, { + await layerManager.createLayer(createLayerName(), LayerType.EMPTY, { insertTop: true, }); } @@ -1173,6 +1184,17 @@ defineExpose({ :activeTool="activeTool" /> + + +
{{ t("Canvas.Scale") }}: {{ currentZoom }}%