From 7a4fc0736d8bc9dc3075dd1626b3afbd10b151b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E9=B9=8F?= <2916022834@qq.com> Date: Thu, 15 Jan 2026 13:42:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=88=E5=A4=9A=E5=BE=88=E5=A4=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/icons/CBrush2.svg | 17 + src/assets/icons/CEraser2.svg | 17 + src/assets/icons/CMarquee.svg | 20 + src/assets/images/canvas/add.png | Bin 0 -> 3749 bytes src/assets/images/canvas/remove.png | Bin 0 -> 3094 bytes src/assets/images/canvas/shubiao-l.png | Bin 0 -> 5355 bytes src/assets/images/canvas/shubiao-r.png | Bin 0 -> 5384 bytes .../components/PartSelectorPanel.vue | 107 +- src/component/Canvas/CanvasEditor/index.vue | 11 +- .../CanvasEditor/managers/PartManager.js | 1216 ++++------------- .../CanvasEditor/managers/ToolManager.js | 43 +- src/component/Canvas/OverallCanvas/demo.vue | 16 +- src/component/Canvas/OverallCanvas/index.vue | 8 + src/component/Canvas/canvasExample.vue | 18 +- src/component/Canvas/test.vue | 2 + 15 files changed, 491 insertions(+), 984 deletions(-) create mode 100644 src/assets/icons/CBrush2.svg create mode 100644 src/assets/icons/CEraser2.svg create mode 100644 src/assets/icons/CMarquee.svg create mode 100644 src/assets/images/canvas/add.png create mode 100644 src/assets/images/canvas/remove.png create mode 100644 src/assets/images/canvas/shubiao-l.png create mode 100644 src/assets/images/canvas/shubiao-r.png diff --git a/src/assets/icons/CBrush2.svg b/src/assets/icons/CBrush2.svg new file mode 100644 index 00000000..d9e38702 --- /dev/null +++ b/src/assets/icons/CBrush2.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/assets/icons/CEraser2.svg b/src/assets/icons/CEraser2.svg new file mode 100644 index 00000000..ea475fa7 --- /dev/null +++ b/src/assets/icons/CEraser2.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/src/assets/icons/CMarquee.svg b/src/assets/icons/CMarquee.svg new file mode 100644 index 00000000..517b2810 --- /dev/null +++ b/src/assets/icons/CMarquee.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/assets/images/canvas/add.png b/src/assets/images/canvas/add.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb5a6030e97d9440116115ec4d447c9f6aa648c GIT binary patch literal 3749 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 SlSkr*Kzf(S@DVo1Y;(H#n+gdz=6N==cF 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 K2w9_PkNY3wjLWJ3 literal 0 HcmV?d00001 diff --git a/src/assets/images/canvas/shubiao-r.png b/src/assets/images/canvas/shubiao-r.png new file mode 100644 index 0000000000000000000000000000000000000000..df70c79d138f37d4780b9550d74c70f65678fab0 GIT binary patch literal 5384 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 literal 0 HcmV?d00001 diff --git a/src/component/Canvas/CanvasEditor/components/PartSelectorPanel.vue b/src/component/Canvas/CanvasEditor/components/PartSelectorPanel.vue index a7f01588..c5ec0856 100644 --- a/src/component/Canvas/CanvasEditor/components/PartSelectorPanel.vue +++ b/src/component/Canvas/CanvasEditor/components/PartSelectorPanel.vue @@ -15,6 +15,20 @@ {{ t("Canvas.GarmentPartSelector") }} +
+
+ + Left Click: Add +
+
+ + Right Click: Remove +
+
@@ -23,9 +37,9 @@ :key="item.type" :class="[ 'tool-btn', - { active: selectionType === item.type }, + { active: toolType === item.type }, ]" - @click="setSelectionType(item.type)" + @click="setPartType(item.type)" > {{ item.label }} @@ -37,24 +51,18 @@
-
+
{{ $t("Canvas.creation") }}
-
+
{{ $t("Canvas.CreateAndCopy") }}
-
- - {{ - $t("Canvas.TheClearlySelectedContent") - }} -
@@ -90,7 +98,7 @@ type: Object, required: true, }, - selectionManager: { + partManager: { type: Object, required: true, }, @@ -115,7 +123,7 @@ // 响应式数据 const visible = ref(false); - const selectionType = ref("rectangle"); + const toolType = ref(OperationType.PART); //打开隐藏操作面板 const closePanel = ref(false); const setClosePanel = () => { @@ -132,20 +140,20 @@ { type: OperationType.PART_RECTANGLE, label: "Marquee Selection", - icon: "CRectangle", - size: "26", + icon: "CMarquee", + size: "20", }, { type: OperationType.PART_BRUSH, label: "Brush Selection", - icon: "CBrush", - size: "24", + icon: "CBrush2", + size: "16", }, { type: OperationType.PART_ERASER, label: "Erase", - icon: "CEraser", - size: "24", + icon: "CEraser2", + size: "22", }, ]; @@ -169,13 +177,13 @@ if (selectionTools.includes(newTool)) { show(); // 根据工具类型设置选区类型 - selectionType.value = newTool; + toolType.value = newTool; // 更新选区管理器的选区类型 - if (props.selectionManager) { - props.selectionManager.setSelectionType(selectionType.value); - props.selectionManager.setupSelectionEvents(); - } + // if (props.partManager) { + // props.partManager.setPartType(toolType.value); + // props.partManager.setupPartEvents(); + // } } else { close(); } @@ -201,20 +209,30 @@ /** * 设置选区类型 */ - function setSelectionType(type) { - selectionType.value = type; + function setPartType(type) { + toolType.value = type; - // 通过 ToolManager 切换工具,这会自动通知 SelectionManager + // 通过 ToolManager 切换工具,这会自动通知 partManager if (props.toolManager) { props.toolManager.setToolWithCommand(type); } - // 备用方案:如果没有 toolManager,直接更新 selectionManager - else if (props.selectionManager) { - props.selectionManager.setSelectionType(type); - props.selectionManager.setupSelectionEvents(); - } + // // 备用方案:如果没有 toolManager,直接更新 partManager + // else if (props.partManager) { + // props.partManager.setPartType(type); + // props.partManager.setupPartEvents(); + // } } + + // 创建 + function onCreate() { + + } + // 复制并创建 + function onCopyCreate() { + + } + diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index d927f742..a5b371eb 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -385,6 +385,8 @@ onMounted(async () => { partManager = new PartManager({ canvas: canvasManager.canvas, layerManager, + canvasManager, + toolManager, }); canvasManager.setPartManager(partManager); @@ -722,8 +724,13 @@ function addRemoveBtn(fun) { }); } -function deleteFun() { - removeLayer(layerManager.activeLayerId.value); +function deleteFun(e, control) { + const target = control.target; + if(target.onDelete){ + target.onDelete(target); + }else if(target.id){ + removeLayer(layerManager.activeLayerId.value); + } } function removeLayer(layerId) { diff --git a/src/component/Canvas/CanvasEditor/managers/PartManager.js b/src/component/Canvas/CanvasEditor/managers/PartManager.js index 4758af83..645d1c8e 100644 --- a/src/component/Canvas/CanvasEditor/managers/PartManager.js +++ b/src/component/Canvas/CanvasEditor/managers/PartManager.js @@ -3,939 +3,295 @@ import { generateId } from "../utils/helper"; import { OperationType } from "../utils/layerHelper"; import { CreateSelectionCommand } from "../commands/SelectionCommands"; import { ClearSelectionCommand } from "../commands/LassoCutoutCommand"; +import addIcon from "@/assets/images/canvas/add.png"; +import removeIcon from "@/assets/images/canvas/remove.png"; /** * 部件选择管理器 */ export class PartManager { - /** - * 创建部件选择管理器 - * @param {Object} options 配置选项 - * @param {Object} options.canvas fabric.js画布实例 - * @param {Object} options.commandManager 命令管理器实例 - * @param {Object} options.layerManager 图层管理实例 - */ - constructor(options = {}) { - this.canvas = options.canvas; - this.commandManager = options.commandManager; - this.layerManager = options.layerManager; - - // 选区状态 - this.isActive = false; - this.selectionType = OperationType.LASSO_RECTANGLE; // 使用常量而不是字符串 - this.selectionObject = null; // 当前选区对象 - this.selectionId = "selection_" + Date.now(); - this.featherAmount = 0; // 羽化值 - - // 选区样式配置 - this.selectionStyle = { - stroke: "#0096ff", - strokeWidth: 1, - strokeDashArray: [5, 5], - fill: "rgba(0, 150, 255, 0.1)", - selectable: false, - evented: false, - excludeFromExport: true, - hoverCursor: "default", - moveCursor: "default", - }; - - // 绘制状态 - this.drawingObject = null; - this.startPoint = null; - this.selectionPath = null; // 存储选区路径数据 - - // 自由选区相关状态 - this.drawingPoints = null; - this.currentPathString = null; - - // 不再直接绑定事件处理函数 - this._mouseDownHandler = null; - this._mouseMoveHandler = null; - this._mouseUpHandler = null; - this._keyDownHandler = null; - - // 选区相关的工具类型 - this.tools = [ - OperationType.PART, - OperationType.PART_RECTANGLE, - OperationType.PART_BRUSH, - OperationType.PART_ERASER, - ]; - - // 当前工具 - this.currentTool = OperationType.SELECT; - - // 选区状态变化回调 - this.onSelectionChanged = null; - - // 不再自动初始化事件,改为手动控制 - // this.initEvents(); - } - - /** - * 设置当前工具 - * @param {String} toolId 工具ID - */ - setCurrentTool(toolId) { - this.currentTool = toolId; - - // 检查是否为选区工具 - const wasActive = this.isActive; - this.isActive = this.tools.includes(toolId); - - // 如果从非选区工具切换到选区工具,初始化事件 - if (!wasActive && this.isActive) { - this.initEvents(); - } - // 如果从选区工具切换到非选区工具,清理事件和选区 - else if (wasActive && !this.isActive) { - this.cleanupEvents(); - this.clearSelection(); - } - - // 根据工具类型设置选区类型 - if (this.isActive) { - this.selectionType = toolId; - } - } - - /** - * 初始化选区相关事件 - */ - initEvents() { - if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化 - - // 保存实例引用,用于事件处理函数中 - const self = this; - - // 鼠标按下事件处理 - this._mouseDownHandler = (options) => { - // 如果选区功能未激活,不处理事件 - if (!this.isActive) return; - - // 如果点击的是已有对象且不是选区对象,则不处理 - if ( - options.target && - options.target.id !== this.selectionId && - options.target.selectable !== false && - options.target.type !== "selection" - ) { - return; - } - - // 阻止事件冒泡,避免与 CanvasEventManager 冲突 - options.e.stopPropagation(); - - // 根据选区类型执行不同的起始操作 - switch (this.selectionType) { - case OperationType.LASSO: - this.startFreeSelection(options); - break; - case OperationType.LASSO_ELLIPSE: - this.startEllipseSelection(options); - break; - case OperationType.LASSO_RECTANGLE: - this.startRectangleSelection(options); - break; - } - }; - - // 鼠标移动事件处理 - this._mouseMoveHandler = (options) => { - // 如果选区功能未激活或没有正在绘制的对象,不处理事件 - if (!this.isActive || !this.drawingObject) return; - - // 阻止事件冒泡 - options.e.stopPropagation(); - - // 根据选区类型执行不同的绘制操作 - switch (this.selectionType) { - case OperationType.LASSO_RECTANGLE: - this.drawRectangleSelection(options); - break; - case OperationType.LASSO_ELLIPSE: - this.drawEllipseSelection(options); - break; - case OperationType.LASSO: - this.drawFreeSelection(options); - break; - } - }; - - // 鼠标抬起事件处理 - this._mouseUpHandler = (options) => { - // 如果选区功能未激活或没有正在绘制的对象,不处理事件 - if (!this.isActive || !this.drawingObject) return; - - // 阻止事件冒泡 - if (options && options.e) { - options.e.stopPropagation(); - } - - // 根据选区类型执行不同的完成操作 - switch (this.selectionType) { - case OperationType.LASSO_RECTANGLE: - this.endRectangleSelection(); - break; - case OperationType.LASSO_ELLIPSE: - this.endEllipseSelection(); - break; - case OperationType.LASSO: - this.endFreeSelection(); - break; - } - - // 如果有命令管理器,使用命令模式记录选区创建 - if (this.commandManager && this.selectionObject) { - this.commandManager.execute( - new CreateSelectionCommand({ - canvas: this.canvas, - selectionManager: this, - selectionObject: this.selectionObject, - selectionType: this.selectionType, - }) - ); - } - }; - - // 键盘事件处理 - this._keyDownHandler = (event) => { - // 只在选区功能激活时处理键盘事件 - if (!this.isActive) return; - - if (event.key === "Escape") { - // ESC键取消当前选区操作 - if (this.drawingObject) { - this.canvas.remove(this.drawingObject); - this.drawingObject = null; - this.startPoint = null; - } - // 清除已有选区 - else if (this.selectionObject) { - if (this.commandManager) { - this.commandManager.execute( - new ClearSelectionCommand({ - selectionManager: this, - }) - ); - } else { - this.clearSelection(); - } - } - } - }; - - // 添加事件监听 - this.canvas.on("mouse:down", this._mouseDownHandler); - this.canvas.on("mouse:move", this._mouseMoveHandler); - this.canvas.on("mouse:up", this._mouseUpHandler); - - // 添加键盘事件监听 - document.addEventListener("keydown", this._keyDownHandler); - } - - /** - * 清理事件监听 - */ - cleanupEvents() { - if (!this.canvas) return; - - // 移除事件监听 - if (this._mouseDownHandler) { - this.canvas.off("mouse:down", this._mouseDownHandler); - this._mouseDownHandler = null; - } - if (this._mouseMoveHandler) { - this.canvas.off("mouse:move", this._mouseMoveHandler); - this._mouseMoveHandler = null; - } - if (this._mouseUpHandler) { - this.canvas.off("mouse:up", this._mouseUpHandler); - this._mouseUpHandler = null; - } - if (this._keyDownHandler) { - document.removeEventListener("keydown", this._keyDownHandler); - this._keyDownHandler = null; - } - } - - /** - * 获取选区对象 - * @returns {Object} 选区对象 - */ - getSelectionObject() { - return this.selectionObject; - } - - /** - * 获取选区路径 - * @returns {Array|String} 选区路径数据 - */ - getSelectionPath() { - return this.selectionPath; - } - - /** - * 获取羽化值 - * @returns {Number} 羽化值 - */ - getFeatherAmount() { - return this.featherAmount; - } - - /** - * 设置羽化值 - * @param {Number} amount 羽化值 - */ - setFeatherAmount(amount) { - this.featherAmount = amount; - return this.updateSelectionAppearance(); - } - - /** - * 设置选区对象 - * @param {Object} object 选区对象 - */ - setSelectionObject(object) { - // 如果已存在选区,先移除 - if (this.selectionObject) { - this.removeSelectionFromCanvas(); - } - - // 更新选区对象 - this.selectionObject = object; - this.selectionPath = object.path; - this.selectionId = object.id || generateId(); - - // 更新外观 - this.updateSelectionAppearance(); - - // 添加到画布(确保在顶层) - if (this.canvas && this.selectionObject) { - this.canvas.add(this.selectionObject); - this.canvas.bringToFront(this.selectionObject); - this.canvas.renderAll(); - } - - // 触发选区变化回调 - if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") { - this.onSelectionChanged(); - } - - return true; - } - - /** - * 从路径数据设置选区 - * @param {Array|String} path 选区路径数据 - */ - setSelectionFromPath(path) { - if (!path) return false; - - // 创建选区对象 - const selectionObj = new fabric.Path(path, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - }); - - // 设置选区 - return this.setSelectionObject(selectionObj); - } - - /** - * 更新选区外观 - */ - updateSelectionAppearance() { - if (!this.selectionObject) return false; - - // 应用基本样式 - Object.assign(this.selectionObject, this.selectionStyle); - - // 应用羽化效果 - if (this.featherAmount > 0) { - this.selectionObject.shadow = new fabric.Shadow({ - color: "rgba(0, 150, 255, 0.5)", - blur: this.featherAmount, - offsetX: 0, - offsetY: 0, - }); - } else { - this.selectionObject.shadow = null; - } - - // 更新画布 - this.canvas.renderAll(); - return true; - } - - /** - * 移除选区 - */ - removeSelectionFromCanvas() { - if (this.canvas && this.selectionObject) { - this.canvas.remove(this.selectionObject); - this.canvas.renderAll(); - } - } - - /** - * 清除选区 - */ - clearSelection() { - // 移除选区对象 - this.removeSelectionFromCanvas(); - - // 重置选区状态 - this.selectionObject = null; - this.selectionPath = null; - this.selectionId = null; - this.featherAmount = 0; - - // 触发选区变化回调 - if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") { - this.onSelectionChanged(); - } - - return true; - } - - /** - * 反转选区 - */ - async invertSelection() { - if (!this.canvas || !this.selectionObject) return false; - - // 获取画布范围 - const canvasRect = new fabric.Rect({ - left: 0, - top: 0, - width: this.canvas.width, - height: this.canvas.height, - selectable: false, - }); - - // 创建反选路径 - let invertedPath; - try { - invertedPath = canvasRect.subtractPathFromRect(this.selectionObject.path); - } catch (error) { - console.error("无法反转选区:", error); - return false; - } - - // 设置新的选区 - const newSelection = new fabric.Path(invertedPath.path, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - }); - - return this.setSelectionObject(newSelection); - } - - /** - * 添加到选区 - * @param {Object} newSelection 要添加的选区对象 - */ - async addToSelection(newSelection) { - if (!this.canvas) return false; - - // 如果当前没有选区,直接使用新选区 - if (!this.selectionObject) { - return this.setSelectionObject(newSelection); - } - - // 合并选区 - let combinedPath; - try { - combinedPath = this.selectionObject.union(newSelection); - } catch (error) { - console.error("无法添加到选区:", error); - return false; - } - - // 设置新的选区 - const combinedSelection = new fabric.Path(combinedPath.path, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - }); - - return this.setSelectionObject(combinedSelection); - } - - /** - * 从选区中移除 - * @param {Object} removeSelection 要移除的选区对象 - */ - async removeFromSelection(removeSelection) { - if (!this.canvas || !this.selectionObject) return false; - - // 从当前选区中减去新选区 - let resultPath; - try { - resultPath = this.selectionObject.subtract(removeSelection); - } catch (error) { - console.error("无法从选区中移除:", error); - return false; - } - - // 设置新的选区 - const newSelection = new fabric.Path(resultPath.path, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - }); - - return this.setSelectionObject(newSelection); - } - - /** - * 应用羽化效果 - * @param {Number} amount 羽化值 - */ - async featherSelection(amount) { - if (!this.selectionObject) return false; - - // 更新羽化值 - this.featherAmount = amount; - - // 更新选区外观 - return this.updateSelectionAppearance(); - } - - /** - * 检查对象是否在选区内 - * @param {Object} object 要检查的对象 - * @returns {Boolean} 是否在选区内 - */ - isObjectInSelection(object) { - if (!this.selectionObject || !object) return false; - - // 获取对象的边界框 - const bounds = object.getBoundingRect(); - const { left, top, width, height } = bounds; - - // 检查对象的中心点和四个角是否在选区内 - const centerX = left + width / 2; - const centerY = top + height / 2; - - // 检查中心点 - if (this.isPointInSelection(centerX, centerY)) return true; - - // 检查四个角 - if (this.isPointInSelection(left, top)) return true; - if (this.isPointInSelection(left + width, top)) return true; - if (this.isPointInSelection(left, top + height)) return true; - if (this.isPointInSelection(left + width, top + height)) return true; - - return false; - } - - /** - * 检查点是否在选区内 - * @param {Number} x X坐标 - * @param {Number} y Y坐标 - * @returns {Boolean} 是否在选区内 - */ - isPointInSelection(x, y) { - if (!this.selectionObject) return false; - - // 使用fabric.js的containsPoint方法判断点是否在选区内 - return this.selectionObject.containsPoint({ x, y }); - } - - /** - * 开始自由选区 - * @param {Object} options 事件对象 - */ - startFreeSelection(options) { - if (!this.canvas || !this.isActive) return; - - // 获取鼠标位置 - const pointer = this.canvas.getPointer(options.e); - this.startPoint = pointer; - - // 创建用于绘制轨迹的点数组 - this.drawingPoints = [pointer]; - - // 初始化SVG路径字符串 - this.currentPathString = `M ${pointer.x} ${pointer.y}`; - - // 创建临时路径对象用于实时显示 - this.drawingObject = new fabric.Path(this.currentPathString, { - stroke: this.selectionStyle.stroke, - strokeWidth: this.selectionStyle.strokeWidth, - strokeDashArray: this.selectionStyle.strokeDashArray, - fill: "transparent", - selectable: false, - evented: false, - strokeLineCap: "round", - strokeLineJoin: "round", - }); - - // 添加到画布 - this.canvas.add(this.drawingObject); - this.canvas.renderAll(); - } - - /** - * 绘制自由选区 - * @param {Object} options 事件对象 - */ - drawFreeSelection(options) { - if (!this.drawingObject || !this.drawingPoints || !this.isActive) return; - - // 获取鼠标位置 - const pointer = this.canvas.getPointer(options.e); - - // 添加新的点,但避免添加过于密集的点 - 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) - ); - - // 只有当距离大于2像素时才添加新点,避免路径过于复杂 - if (distance > 2) { - this.drawingPoints.push(pointer); - - // 更新路径字符串 - this.currentPathString += ` L ${pointer.x} ${pointer.y}`; - - // 移除旧的绘制对象 - this.canvas.remove(this.drawingObject); - - // 创建新的路径对象 - this.drawingObject = new fabric.Path(this.currentPathString, { - stroke: this.selectionStyle.stroke, - strokeWidth: this.selectionStyle.strokeWidth, - strokeDashArray: this.selectionStyle.strokeDashArray, - fill: "transparent", - selectable: false, - evented: false, - strokeLineCap: "round", - strokeLineJoin: "round", - }); - - // 重新添加到画布 - this.canvas.add(this.drawingObject); - this.canvas.renderAll(); - } - } - - /** - * 结束自由选区 - */ - endFreeSelection() { - if (!this.drawingObject || !this.drawingPoints || !this.isActive) return; - - // 检查是否有足够的点来形成选区 - if (this.drawingPoints.length < 3) { - // 点太少,清除绘制对象 - this.canvas.remove(this.drawingObject); - this.drawingObject = null; - this.drawingPoints = null; - this.startPoint = null; - this.currentPathString = null; - return; - } - - // 自动闭合路径 - 连接最后一点到第一点 - 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) - ); - - // 如果首尾距离较大,自动添加闭合线段 - let finalPathString = this.currentPathString; - if (closingDistance > 10) { - finalPathString += ` L ${firstPoint.x} ${firstPoint.y}`; - } - finalPathString += " Z"; // 闭合路径 - - // 创建最终选区对象 - const selectionObj = new fabric.Path(finalPathString, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - fill: this.selectionStyle.fill, // 恢复填充 - }); - - // 移除绘制中的临时对象 - this.canvas.remove(this.drawingObject); - - // 重置绘制状态 - this.drawingObject = null; - this.drawingPoints = null; - this.startPoint = null; - this.currentPathString = null; - - // 设置选区 - this.setSelectionObject(selectionObj); - } - - /** - * 开始矩形选区 - * @param {Object} options 事件对象 - */ - startRectangleSelection(options) { - if (!this.canvas || !this.isActive) return; - - // 获取鼠标位置 - const pointer = this.canvas.getPointer(options.e); - this.startPoint = pointer; - - // 创建矩形对象 - this.drawingObject = new fabric.Rect({ - left: pointer.x, - top: pointer.y, - width: 0, - height: 0, - ...this.selectionStyle, - fill: "transparent", // 在绘制过程中不显示填充 - }); - - // 添加到画布 - this.canvas.add(this.drawingObject); - this.canvas.renderAll(); - } - - /** - * 绘制矩形选区 - * @param {Object} options 事件对象 - */ - drawRectangleSelection(options) { - if (!this.drawingObject || !this.startPoint || !this.isActive) return; - - // 获取鼠标位置 - const pointer = this.canvas.getPointer(options.e); - - // 计算宽度和高度 - const width = Math.abs(pointer.x - this.startPoint.x); - const height = Math.abs(pointer.y - this.startPoint.y); - - // 确定左上角坐标 - const left = Math.min(this.startPoint.x, pointer.x); - const top = Math.min(this.startPoint.y, pointer.y); - - // 更新矩形 - this.drawingObject.set({ - left: left, - top: top, - width: width, - height: height, - }); - - this.canvas.renderAll(); - } - - /** - * 结束矩形选区 - */ - endRectangleSelection() { - if (!this.drawingObject || !this.startPoint || !this.isActive) return; - - // 将矩形转换为路径 - const left = this.drawingObject.left; - const top = this.drawingObject.top; - const width = this.drawingObject.width; - const height = this.drawingObject.height; - - // 如果矩形太小,忽略 - if (width < 5 || height < 5) { - this.canvas.remove(this.drawingObject); - this.drawingObject = null; - this.startPoint = null; - return; - } - - // 创建矩形路径字符串 - const pathString = `M ${left} ${top} L ${left + width} ${top} L ${ - left + width - } ${top + height} L ${left} ${top + height} Z`; - - // 创建最终选区对象 - const selectionObj = new fabric.Path(pathString, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - fill: this.selectionStyle.fill, // 恢复填充 - }); - - // 移除绘制中的临时对象 - this.canvas.remove(this.drawingObject); - - // 重置绘制状态 - this.drawingObject = null; - this.startPoint = null; - - // 设置选区 - this.setSelectionObject(selectionObj); - } - - /** - * 开始椭圆选区 - * @param {Object} options 事件对象 - */ - startEllipseSelection(options) { - if (!this.canvas || !this.isActive) return; - - // 获取鼠标位置 - const pointer = this.canvas.getPointer(options.e); - this.startPoint = pointer; - - // 创建椭圆对象 - this.drawingObject = new fabric.Ellipse({ - left: pointer.x, - top: pointer.y, - rx: 0, - ry: 0, - ...this.selectionStyle, - fill: "transparent", // 在绘制过程中不显示填充 - // originX: "left", - // originY: "top", - originX: "center", - originY: "center", - }); - - // 添加到画布 - this.canvas.add(this.drawingObject); - this.canvas.renderAll(); - } - - /** - * 绘制椭圆选区 - * @param {Object} options 事件对象 - */ - drawEllipseSelection(options) { - if (!this.drawingObject || !this.startPoint || !this.isActive) return; - - // 获取鼠标位置 - const pointer = this.canvas.getPointer(options.e); - - // 计算半径 - const rx = Math.abs(pointer.x - this.startPoint.x) / 2; - const ry = Math.abs(pointer.y - this.startPoint.y) / 2; - - // 确定中心坐标 - const left = Math.min(this.startPoint.x, pointer.x); - const top = Math.min(this.startPoint.y, pointer.y); - - // 更新椭圆 - this.drawingObject.set({ - left: left, - top: top, - rx: rx, - ry: ry, - originX: "left", - originY: "top", - }); - - this.canvas.renderAll(); - } - - /** - * 结束椭圆选区 - */ - endEllipseSelection() { - if (!this.drawingObject || !this.startPoint || !this.isActive) return; - - // 获取椭圆参数 - const { left, top, rx, ry } = this.drawingObject; - - // 如果椭圆太小,忽略 - if (rx < 2 || ry < 2) { - this.canvas.remove(this.drawingObject); - this.drawingObject = null; - this.startPoint = null; - return; - } - - // 计算中心点 - const cx = left + rx; - const cy = top + ry; - - // 将椭圆转换为路径字符串 - const pathString = this.ellipseToSVGPath(cx, cy, rx, ry); - - // 创建最终选区对象 - const selectionObj = new fabric.Path(pathString, { - ...this.selectionStyle, - id: `selection_${Date.now()}`, - name: "selection", - fill: this.selectionStyle.fill, // 恢复填充 - }); - - // 移除绘制中的临时对象 - this.canvas.remove(this.drawingObject); - - // 重置绘制状态 - this.drawingObject = null; - this.startPoint = null; - - // 设置选区 - this.setSelectionObject(selectionObj); - } - - /** - * 将椭圆转换为SVG路径字符串 - * @param {Number} cx 中心点X坐标 - * @param {Number} cy 中心点Y坐标 - * @param {Number} rx X半径 - * @param {Number} ry Y半径 - * @returns {String} SVG路径字符串 - */ - ellipseToSVGPath(cx, cy, rx, ry) { - // 使用椭圆弧命令创建完整椭圆 - return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${ - cx + rx - } ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`; - } - - /** - * 设置选区工具 - * @param {string} type 选区类型:OperationType.LASSO, OperationType.LASSO_RECTANGLE, OperationType.LASSO_ELLIPSE - */ - setSelectionType(type) { - this.selectionType = type; - - // 如果正在绘制,清除临时对象 - if (this.drawingObject) { - this.canvas.remove(this.drawingObject); - this.drawingObject = null; - this.startPoint = null; - } - } - - /** - * 设置选区工具的鼠标事件 - */ - setupSelectionEvents() { - // 选区事件现在通过 setCurrentTool 方法管理 - // 这个方法现在主要用于刷新或重置事件监听 - if (!this.canvas || !this.isActive) return; - - // 确保选区处于激活状态 - if (this.tools.includes(this.currentTool)) { - this.isActive = true; - // 如果事件还没有初始化,初始化它们 - if (!this._mouseDownHandler) { - this.initEvents(); - } - } - } - - /** - * 清理资源 - */ - dispose() { - this.cleanupEvents(); - this.clearSelection(); - this.canvas = null; - this.commandManager = null; - this.layerManager = null; - } + /** + * 创建部件选择管理器 + * @param {Object} options 配置选项 + * @param {Object} options.canvas fabric.js画布实例 + * @param {Object} options.commandManager 命令管理器实例 + * @param {Object} options.canvasManager 画布管理实例 + * @param {Object} options.layerManager 图层管理实例 + * @param {Object} options.toolManager 工具管理实例 + */ + constructor(options = {}) { + this.canvas = options.canvas; + this.commandManager = options.commandManager; + this.layerManager = options.layerManager; + this.canvasManager = options.canvasManager; + this.toolManager = options.toolManager; + + // 状态 + this.isActive = false; + this.partObject = null; // 当前选区对象 + this.partId = "part_selector"; + this.defaultCursor = "default"; + + // 绘制状态 + this.drawingObject = null; + this.startPoint = null; + this.partPath = null; // 存储选区路径数据 + + + // 不再直接绑定事件处理函数 + this._mouseDownHandler = null; + this._mouseMoveHandler = null; + this._mouseUpHandler = null; + this._keyDownHandler = null; + + // 选区相关的工具类型 + this.tools = [ + OperationType.PART, + OperationType.PART_RECTANGLE, + OperationType.PART_BRUSH, + OperationType.PART_ERASER, + ]; + + // 当前工具 + this.activeTool = this.toolManager.activeTool; + + // 选区状态变化回调 + this.onSelectionChanged = null; + } + + /** + * 设置当前工具 + * @param {String} toolId 工具ID + */ + setCurrentTool(toolId) { + // 检查是否为选区工具 + const wasActive = this.isActive; + this.isActive = this.tools.includes(toolId); + + // 如果从非选区工具切换到选区工具,初始化事件 + if (!wasActive && this.isActive) { + this.initEvents(); + } + // 如果从选区工具切换到非选区工具,清理事件和选区 + else if (wasActive && !this.isActive) { + this.cleanupEvents(); + this.clearSelection(); + } + } + + /** + * 初始化选区相关事件 + */ + initEvents() { + if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化 + this.defaultCursor = this.canvas.defaultCursor; + + // 保存实例引用,用于事件处理函数中 + const self = this; + + // 鼠标按下事件处理 + this._mouseDownHandler = (options) => { + // 如果选区功能未激活,不处理事件 + if (!this.isActive) return; + // 阻止事件冒泡,避免与 CanvasEventManager 冲突 + options.e.stopPropagation(); + switch (this.activeTool.value) { + case OperationType.PART: + this._pointDownkHandler(options); + break; + case OperationType.PART_RECTANGLE: + this._rectangleDownHandler(options); + break; + case OperationType.PART_BRUSH: + this._brushDownHandler(options); + break; + case OperationType.PART_ERASER: + this._eraseDownHandler(options); + break; + + default: + break; + } + }; + + // 鼠标移动事件处理 + this._mouseMoveHandler = (options) => { + // 如果选区功能未激活或没有正在绘制的对象,不处理事件 + if (!this.isActive) return; + // 阻止事件冒泡 + options.e.stopPropagation(); + switch (this.activeTool.value) { + case OperationType.PART: + this._pointMoveHandler(options); + break; + case OperationType.PART_RECTANGLE: + this._rectangleMoveHandler(options); + break; + case OperationType.PART_BRUSH: + this._brushMoveHandler(options); + break; + case OperationType.PART_ERASER: + this._eraseMoveHandler(options); + break; + + default: + break; + } + }; + + // 鼠标抬起事件处理 + this._mouseUpHandler = (options) => { + // 如果选区功能未激活或没有正在绘制的对象,不处理事件 + if (!this.isActive) return; + // 阻止事件冒泡 + if (options && options.e) { + options.e.stopPropagation(); + } + switch (this.activeTool.value) { + case OperationType.PART: + this._pointUpHandler(options); + break; + case OperationType.PART_RECTANGLE: + this._rectangleUpHandler(options); + break; + case OperationType.PART_BRUSH: + this._brushUpHandler(options); + break; + case OperationType.PART_ERASER: + this._eraseUpHandler(options); + break; + + default: + break; + } + }; + + // 键盘事件处理 + this._keyDownHandler = (event) => { + // 只在选区功能激活时处理键盘事件 + if (!this.isActive) return; + }; + + // 添加事件监听 + this.canvas.on("mouse:down", this._mouseDownHandler); + this.canvas.on("mouse:move", this._mouseMoveHandler); + this.canvas.on("mouse:up", this._mouseUpHandler); + + // 添加键盘事件监听 + document.addEventListener("keydown", this._keyDownHandler); + } + + /** + * 清理事件监听 + */ + cleanupEvents() { + if (!this.canvas) return; + + // 移除事件监听 + if (this._mouseDownHandler) { + this.canvas.off("mouse:down", this._mouseDownHandler); + this._mouseDownHandler = null; + } + if (this._mouseMoveHandler) { + this.canvas.off("mouse:move", this._mouseMoveHandler); + this._mouseMoveHandler = null; + } + if (this._mouseUpHandler) { + this.canvas.off("mouse:up", this._mouseUpHandler); + this._mouseUpHandler = null; + } + if (this._keyDownHandler) { + document.removeEventListener("keydown", this._keyDownHandler); + this._keyDownHandler = null; + } + } + + // 点选工具模式下点击事件处理 + _pointDownkHandler(options) { + const button = options.button; + const isLeft = button === 1;// 左键1(添加) 右键3(删除) + const icon = `url("${isLeft ? addIcon : removeIcon}") 16 16, default` + this.canvas.upperCanvasEl.style.cursor = icon; + } + // 点选工具模式下移动事件处理 + _pointMoveHandler(options) { + + } + // 点选工具模式下抬起事件处理 + _pointUpHandler(options) { + const button = options.button; + const isLeft = button === 1;// 左键1(添加) 右键3(删除) + this.canvas.upperCanvasEl.style.cursor = this.defaultCursor; + const { x, y } = options.pointer; + const fixedObject = this.canvasManager.getFixedLayerObject({ x, y }); + console.log("==========", fixedObject) + } + + + // 框选工具模式下点击事件处理 + _rectangleDownHandler(options) { + } + // 框选工具模式下移动事件处理 + _rectangleMoveHandler(options) { + + } + // 框选工具模式下抬起事件处理 + _rectangleUpHandler(options) { + } + + + // 绘制工具模式下点击事件处理 + _brushDownHandler(options) { + } + // 绘制工具模式下移动事件处理 + _brushMoveHandler(options) { + + } + // 绘制工具模式下抬起事件处理 + _brushUpHandler(options) { + } + + + // 擦除工具模式下抬起事件处理 + _eraseUpHandler(options) { + } + // 擦除工具模式下点击事件处理 + _eraseDownHandler(options) { + } + // 擦除工具模式下移动事件处理 + _eraseMoveHandler(options) { + + } + + + + /** + * 清除选区 + */ + clearSelection() { + // 移除选区对象 + // this.removeSelectionFromCanvas(); + + // 重置选区状态 + this.partObject = null; + this.partPath = null; + + // 触发选区变化回调 + if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") { + this.onSelectionChanged(); + } + + return true; + } + + /** + * 清理资源 + */ + dispose() { + this.cleanupEvents(); + this.clearSelection(); + this.canvas = null; + this.commandManager = null; + this.layerManager = null; + } } diff --git a/src/component/Canvas/CanvasEditor/managers/ToolManager.js b/src/component/Canvas/CanvasEditor/managers/ToolManager.js index db048368..3f8949f9 100644 --- a/src/component/Canvas/CanvasEditor/managers/ToolManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ToolManager.js @@ -197,19 +197,19 @@ export class ToolManager { name: "部件选取工具-矩形", icon: "part", cursor: "default", - setup: this.setupPartTool.bind(this), + setup: this.setupPartRectangleTool.bind(this), }, [OperationType.PART_BRUSH]: { name: "部件选取工具-画笔", icon: "part", cursor: "default", - setup: this.setupPartTool.bind(this), + setup: this.setupPartBrushTool.bind(this), }, [OperationType.PART_ERASER]: { name: "部件选取工具-橡皮擦", icon: "part", cursor: "default", - setup: this.setupPartTool.bind(this), + setup: this.setupPartEraserTool.bind(this), }, // 红绿图模式专用工具 @@ -705,14 +705,47 @@ export class ToolManager { */ setupPartTool() { if (!this.canvas) return; - if (this.checkToolCanOperateSelectedObject()) return; this.canvas.isDrawingMode = false; this.canvas.selection = false; - + if (this.canvasManager && this.canvasManager.partManager) { this.canvasManager.partManager.setCurrentTool(OperationType.PART); } } + /** + * 设置部件选取工具--矩形 + */ + setupPartRectangleTool() { + if (!this.canvas) return; + this.canvas.isDrawingMode = false; + this.canvas.selection = true; + if (this.canvasManager && this.canvasManager.partManager) { + this.canvasManager.partManager.setCurrentTool(OperationType.PART_RECTANGLE); + } + } + /** + * 设置部件选取工具--画笔 + */ + setupPartBrushTool() { + if (!this.canvas) return; + this.canvas.isDrawingMode = true; + this.canvas.selection = false; + if (this.canvasManager && this.canvasManager.partManager) { + this.canvasManager.partManager.setCurrentTool(OperationType.PART_BRUSH); + } + } + /** + * 设置部件选取工具--橡皮擦 + */ + setupPartEraserTool() { + if (!this.canvas) return; + this.canvas.isDrawingMode = false; + this.canvas.selection = false; + if (this.canvasManager && this.canvasManager.partManager) { + this.canvasManager.partManager.setCurrentTool(OperationType.PART_ERASER); + } + } + /** * 设置波浪工具 diff --git a/src/component/Canvas/OverallCanvas/demo.vue b/src/component/Canvas/OverallCanvas/demo.vue index dd5331e5..8c378d4b 100644 --- a/src/component/Canvas/OverallCanvas/demo.vue +++ b/src/component/Canvas/OverallCanvas/demo.vue @@ -79,7 +79,9 @@ type="number" v-model="item.object.scaleX" step="0.1" - @input="updateList(item, 'object.scaleX', item.object.scaleX)" + @input=" + updateList(item, 'object.scaleX', item.object.scaleX) + " />
@@ -88,7 +90,9 @@ type="number" v-model="item.object.scaleY" step="0.1" - @input="updateList(item, 'object.scaleY', item.object.scaleY)" + @input=" + updateList(item, 'object.scaleY', item.object.scaleY) + " />
@@ -124,7 +128,9 @@ step="0.1" min="0" max="1" - @input="updateList(item, 'object.opacity', item.object.opacity)" + @input=" + updateList(item, 'object.opacity', item.object.opacity) + " />
@@ -228,6 +234,8 @@ } } else if (item.action === ACTIONS.SELECT) { activeToken.value = item.token; + } else if (item.action === ACTIONS.DELETE) { + list.value = list.value.filter((v) => v.token !== item.token); } }); }; @@ -284,7 +292,7 @@ }; // 监听列表变化属性变更 const updateList = (item, key, value) => { - if(key === "scale[0]") item.scale[1] = value; + if (key === "scale[0]") item.scale[1] = value; pingpuRef.value.updataList([ { token: item.token, diff --git a/src/component/Canvas/OverallCanvas/index.vue b/src/component/Canvas/OverallCanvas/index.vue index 0bc44351..2feedef1 100644 --- a/src/component/Canvas/OverallCanvas/index.vue +++ b/src/component/Canvas/OverallCanvas/index.vue @@ -144,6 +144,13 @@ const list = [{ token, action: ACTIONS.SELECT }]; emit("change-canvas", list); }; + // 删除对象 + const onDeleteItem = (object) => { + const list = [{ token: object.token, action: ACTIONS.DELETE }]; + emit("change-canvas", list); + canvas.remove(object); + canvas.renderAll(); + }; const urlToCanvas = (url) => { return new Promise((resolve, reject) => { fabric.Image.fromURL( @@ -181,6 +188,7 @@ height: cheight, fill: pattern, ...item.object, + onDelete: (v) => onDeleteItem(v), }); canvas.add(rect); }; diff --git a/src/component/Canvas/canvasExample.vue b/src/component/Canvas/canvasExample.vue index 4d270e12..be04b46a 100644 --- a/src/component/Canvas/canvasExample.vue +++ b/src/component/Canvas/canvasExample.vue @@ -335,15 +335,15 @@ const otherData = { color: {rgba: {r:255,g:0,b:0,a:1}}, printObject: { prints: [ - { - ifSingle: false, - level2Type: "Pattern", - designType: "Library", - path: "/src/assets/images/canvas/yinhua1.jpg", - location: [250, 780], - scale: [0.3, 0.4], - angle: 0, - }, + // { + // ifSingle: false, + // level2Type: "Pattern", + // designType: "Library", + // path: "/src/assets/images/canvas/yinhua1.jpg", + // location: [250, 780], + // scale: [0.3, 0.4], + // angle: 0, + // }, { ifSingle: true, level2Type: "Pattern", diff --git a/src/component/Canvas/test.vue b/src/component/Canvas/test.vue index 097f2571..1433898e 100644 --- a/src/component/Canvas/test.vue +++ b/src/component/Canvas/test.vue @@ -5,11 +5,13 @@ --> +