import { fabric } from "fabric-with-all"; import { traceImageContour, imageToCanvas } from "../utils/helper"; import { OperationType } from "../utils/layerHelper"; import { LassoCutoutCommand } from "../commands/LassoCutoutCommand"; import addIcon from "@/assets/images/canvas/add.png"; import removeIcon from "@/assets/images/canvas/remove.png"; import { getObjectAlphaToCanvas } from "../utils/objectHelper"; import { Https } from "@/tool/https"; import { PartDrawCommand } from "../commands/PartCommands"; /** * 部件选择管理器 */ export class PartManager { /** * 创建部件选择管理器 * @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.selectionManager = options.selectionManager; this.layerManager = options.layerManager; this.canvasManager = options.canvasManager; this.toolManager = options.toolManager; this.props = options.props; // 选区样式配置 this.selectionStyle = { stroke: "#0096ff", strokeWidth: 1, strokeDashArray: [5, 5], fill: "rgba(0, 150, 255, 0.1)", strokeUniform: true, // 保持描边宽度不随缩放改变 // fill: "rgba(127, 255, 127, 0.3)", // stroke: "#2AA81B", // strokeWidth: 2, // strokeDashArray: [8, 4], // strokeLineCap: "round",// 折线端点样式 // strokeLineJoin: "bevel", // 折线连接样式 selectable: false, evented: false, excludeFromExport: true, hoverCursor: "default", moveCursor: "default", }; // 状态 this.isActive = false; // 不再直接绑定事件处理函数 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.rgba = { r: 0, g: 255, b: 0, a: 200 }; this.partId = "part_selector"; this.partGroup = null; // 当前选区对象 this.partCanvas = null;// 选区画布 this.rectangleObject = null; // 矩形对象 this.pointList = []; // 点位列表 存储点选坐标 } /** * 设置当前工具 * @param {String} toolId 工具ID */ setCurrentTool(toolId) { // 检查是否为选区工具 const wasActive = this.isActive; this.isActive = this.tools.includes(toolId); if (toolId === OperationType.PART_ERASER) { this.setEraserTool(); } else if (toolId === OperationType.PART || toolId === OperationType.PART_RECTANGLE) { this.clearPointData(); this.resetPartObject(); } if (toolId === OperationType.PART_ERASER || toolId === OperationType.PART_BRUSH) { if (this.pointList.length > 0) { this.clearPointData(); this.resetPartObject(); } } // 如果从非选区工具切换到选区工具,初始化事件 if (!wasActive && this.isActive) { this.initEvents(); this.createPartObject(); } // 如果从选区工具切换到非选区工具,清理事件和选区 else if (wasActive && !this.isActive) { this.cleanupEvents(); this.clearPartObject(); this.clearPointData(); } // 如果从选区工具切换到选区工具,重置选区 else if (wasActive && this.isActive) { } } /** 初始化选区相关事件 */ initEvents() { if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化 // 保存实例引用,用于事件处理函数中 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) { } /** 点选工具模式下抬起事件处理 */ async _pointUpHandler(options) { const button = options.button; const isLeft = button === 1;// 左键1(添加) 右键3(删除) const fixedObject = this.canvasManager.getFixedLayerObject(); if (!fixedObject) return console.warn("未找到固定图层"); const { x, y } = this.handleMousePosition(options, fixedObject); const label = isLeft ? 1 : 0; const points = []; const labels = []; this.pointList.forEach((item) => { points.push([item.x, item.y]); labels.push(item.label); }); points.push([x, y]); labels.push(label); const url = await this.getSegAnythingImage({ type: "point", points, labels, }); this.pointList.push({ x: x, y: y, label: label, }) const image1 = await this.loadImageToObject(url); this.resetPartObject(); const group = this.partGroup; const canvas = getObjectAlphaToCanvas(image1, null, 0, this.rgba); this.partCanvas = canvas; const image2 = new fabric.Image(canvas); image2.set({ originX: fixedObject.originX, originY: fixedObject.originY, }); group.add(image2); for (let i = 0; i < this.pointList.length; i++) { const item = this.pointList[i]; const icon = await this.loadImageToObject(item.label === 1 ? addIcon : removeIcon); icon.set({ left: item.x - group.width / 2, top: item.y - group.height / 2, originX: fixedObject.originX, originY: fixedObject.originY, }) group.add(icon); } this.canvas.renderAll(); } /** 清空点选数据 */ clearPointData() { this.pointList = []; this.partCanvas = null; } /** 框选工具模式下点击事件处理 */ _rectangleDownHandler(options) { this.clearPointData(); const fixedObject = this.canvasManager.getFixedLayerObject(); if (!fixedObject) return console.warn("未找到固定图层"); const { x, y } = this.handleMousePosition(options, fixedObject); this.pointList.push(x, y); this.rectangleObject = new fabric.Rect({ left: x - fixedObject.width / 2, top: y - fixedObject.height / 2, width: 0, height: 0, ...this.selectionStyle, fill: "transparent", // 在绘制过程中不显示填充 strokeUniform: true, }); this.partGroup.add(this.rectangleObject); this.canvas.renderAll(); } /** 框选工具模式下移动事件处理 */ _rectangleMoveHandler(options) { if (!this.rectangleObject) return console.warn("未找到框选对象"); const fixedObject = this.canvasManager.getFixedLayerObject(); if (!fixedObject) return console.warn("未找到固定图层"); const { x, y } = this.handleMousePosition(options, fixedObject); this.rectangleObject.set({ width: x - this.rectangleObject.left - fixedObject.width / 2, height: y - this.rectangleObject.top - fixedObject.height / 2, }); this.canvas.renderAll(); } /** 框选工具模式下抬起事件处理 */ async _rectangleUpHandler(options) { if (this.rectangleObject) { this.partGroup.remove(this.rectangleObject); this.canvas.renderAll(); } const fixedObject = this.canvasManager.getFixedLayerObject(); if (!fixedObject) return console.warn("未找到固定图层"); const { x, y } = this.handleMousePosition(options, fixedObject); this.pointList.push(x, y); if (this.pointList.length !== 4) return console.warn("框选工具选择区域必须是矩形"); const url = await this.getSegAnythingImage({ type: "box", box: [...this.pointList], }); const image = await this.loadImageToObject(url); const canvas = getObjectAlphaToCanvas(image, null, 0, this.rgba); this.drawPartCanvas(canvas); } /** 获取分隔后图片 */ async getSegAnythingImage(obj) { setTimeout(() => { this.canvas.loading.value = true; }); return new Promise((resolve, reject) => { // const user_id = store.state.UserHabit.userDetail.userId; const user_id = 24299; const data = { image_path: this.props.clothingMinIOPath, user_id, ...obj, } Https.axiosPost(Https.httpUrls.segAnything, data) .then(response => { this.canvas.loading.value = false; if (response) { resolve(response); } else { console.error("获取分隔后图片失败"); } }) .catch(error => { this.canvas.loading.value = false; console.error(error); }); }); } /** 处理鼠标点位 */ handleMousePosition(options, fixedObject) { const pos = options.absolutePointer; const { x, y } = options.absolutePointer; const width = fixedObject.width * fixedObject.scaleX; const height = fixedObject.height * fixedObject.scaleY; const X = (x - (fixedObject.left - width / 2)) / fixedObject.scaleX; const Y = (y - (fixedObject.top - height / 2)) / fixedObject.scaleY; return { x: Math.round(X), y: Math.round(Y), } } /** 绘制工具模式下点击事件处理 */ _brushDownHandler(options) { } /** 绘制工具模式下移动事件处理 */ _brushMoveHandler(options) { } /** 绘制工具模式下抬起事件处理 */ _brushUpHandler(options) { } /** 绘制模式添加画笔 */ async addDrawPartImage(fabricImage) { const scaleX = fabricImage.scaleX / this.partGroup.scaleX; const scaleY = fabricImage.scaleY / this.partGroup.scaleY; const top = (fabricImage.top - this.partGroup.top) / this.partGroup.scaleY; const left = (fabricImage.left - this.partGroup.left) / this.partGroup.scaleX; fabricImage.set({ scaleX, scaleY, top: top + this.partGroup.height / 2, left: left + this.partGroup.width / 2, }) const tcanvas = new fabric.StaticCanvas(document.createElement("canvas"), { width: this.partGroup.width, height: this.partGroup.height, enableRetinaScaling: false, }); if (this.partCanvas) { let image = new fabric.Image(this.partCanvas); tcanvas.add(image) } tcanvas.add(fabricImage) tcanvas.renderAll(); const canvas = getObjectAlphaToCanvas(tcanvas, null, 0, this.rgba); const cmd = new PartDrawCommand({ canvas: this.canvas, partManager: this, partCanvas: canvas, }) if (this.commandManager?.execute) { this.commandManager.execute(cmd); } else { cmd.execute(); } } /** 切换到擦除工具 */ setEraserTool() { if (!this.canvas) return console.warn("未找到画布"); const objects = this.canvas.getObjects(); objects.forEach(obj => { if (obj.id === this.partId) { obj.set({ erasable: true }) } }) } /** 擦除工具模式下擦除完成事件处理 */ async onErasingEnd(affectedObjects) { console.log("擦除完成", affectedObjects); const tcanvas = new fabric.StaticCanvas(document.createElement("canvas"), { width: this.partGroup.width, height: this.partGroup.height, enableRetinaScaling: false, }); await new Promise((resolve, reject) => { this.partGroup.clone((clone) => { clone.set({ scaleX: 1, scaleY: 1, top: this.partGroup.height / 2, left: this.partGroup.width / 2, }) tcanvas.add(clone); resolve(clone); }) }); tcanvas.renderAll(); const canvas = getObjectAlphaToCanvas(tcanvas, null, 0, this.rgba); const cmd = new PartDrawCommand({ canvas: this.canvas, partManager: this, partCanvas: canvas, }) if (this.commandManager?.execute) { this.commandManager.execute(cmd); } else { cmd.execute(); } } /** 擦除工具模式下点击事件处理 */ _eraseDownHandler(options) { } /** 擦除工具模式下移动事件处理 */ _eraseMoveHandler(options) { } /** 擦除工具模式下抬起事件处理 */ _eraseUpHandler(options) { } /** 绘制部件画布 */ drawPartCanvas(canvas) { this.partCanvas = canvas; const image = new fabric.Image(canvas); image.set({ originX: this.partGroup.originX, originY: this.partGroup.originY, erasable: true, }); this.resetPartObject(); this.partGroup.add(image); this.canvas.renderAll(); } /** 删除指定ID的对象 */ removeObjectsById(id) { const objects = this.canvas.getObjects().filter(obj => obj.id === id); this.canvas.remove(...objects); } loadImageToObject(url) { return new Promise((resolve, reject) => { fabric.Image.fromURL(url, (img) => { resolve(img); }, { crossOrigin: "anonymous" });// 防止污染 }); } /** 重置点位对象组 */ resetPartObject(render = false) { this.clearPartObject(); this.createPartObject(); if (render) this.canvas.renderAll(); } createPartObject() { const fixedObject = this.canvasManager.getFixedLayerObject(); if (!fixedObject) return console.warn("未找到固定图层"); const group = new fabric.Group(); group.set({ id: this.partId, opacity: 1, left: fixedObject.left, top: fixedObject.top, width: fixedObject.width, height: fixedObject.height, scaleX: fixedObject.scaleX, scaleY: fixedObject.scaleY, originX: fixedObject.originX, originY: fixedObject.originY, selectable: false, evented: false, erasable: true, }) this.canvas.add(group); this.partGroup = group; } /** 清空点位对象组 */ clearPartObject() { this.removeObjectsById(this.partId); this.partGroup = null; } /** 创建当前选区 */ async createPart() { if (!this.partCanvas) return console.warn("没有点位画布"); const fixedObject = this.canvasManager.getFixedLayerObject(); if (!fixedObject) return console.warn("未找到固定图层"); // const tcanvas = new fabric.StaticCanvas(document.createElement("canvas"), { // width: fixedObject.width, // height: fixedObject.height, // enableRetinaScaling: false, // }); // await new Promise((resolve, reject) => { // fixedObject.clone((clone) => { // const clipPath = new fabric.Image(this.partCanvas); // clipPath.set({ // originX: fixedObject.originX, // originY: fixedObject.originY, // }) // clone.set({ // scaleX: 1, // scaleY: 1, // clipPath: clipPath, // }) // tcanvas.add(clone); // resolve(clone); // }) // }); // tcanvas.renderAll(); const scaleY = fixedObject.scaleY const scaleX = fixedObject.scaleX const top = fixedObject.top - fixedObject.height * scaleY / 2; const left = fixedObject.left - fixedObject.width * scaleX / 2; const arr = traceImageContour(this.partCanvas); let minX = fixedObject.width; let minY = fixedObject.height; const str = arr.map((v) => { if (v.x < minX) minX = v.x; if (v.y < minY) minY = v.y; return `${v.x} ${v.y}` }).join(" L "); const path = new fabric.Path(`M ${str} z`); path.set({ left: left + minX * scaleX, top: top + minY * scaleY, scaleX: scaleX, scaleY: scaleY, ...this.selectionStyle, }); this.selectionManager.setSelectionObject(path); this.clearPart(); const cmd = new LassoCutoutCommand({ canvas: this.canvas, layerManager: this.layerManager, selectionManager: this.selectionManager, toolManager: this.toolManager, }) this.commandManager.execute(cmd); } /** 清空点位 */ clearPart() { this.pointList = []; this.partCanvas = null; this.resetPartObject(true); } /** * 清理资源 */ dispose() { this.cleanupEvents(); this.clearPartObject(); this.clearPointData(); this.canvas = null; this.commandManager = null; this.layerManager = null; } }