From eccc00dc532148187b150679929e1cbedc223e80 Mon Sep 17 00:00:00 2001 From: lzp Date: Mon, 23 Mar 2026 16:43:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E7=94=BB=E5=B8=83=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E9=80=89=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/layer-panel/layer-item.vue | 192 ++++++++------- .../components/layer-panel/layer-list.vue | 118 +++++++-- .../manager/AISelectboxToolManager.ts | 223 ++---------------- .../DepthCanvas/manager/CanvasManager.ts | 70 ++++-- .../DepthCanvas/manager/LayerManager.ts | 126 ++++++---- .../DepthCanvas/manager/ShapeToolManager.ts | 12 +- .../DepthCanvas/manager/StateManager.ts | 1 + .../manager/events/CanvasEventManager.js | 4 +- .../Canvas/DepthCanvas/tools/canvasMethod.js | 154 ++++++++++++ .../Canvas/DepthCanvas/tools/exportMethod.js | 6 + 10 files changed, 532 insertions(+), 374 deletions(-) diff --git a/src/components/Canvas/DepthCanvas/components/layer-panel/layer-item.vue b/src/components/Canvas/DepthCanvas/components/layer-panel/layer-item.vue index fddf1d4..e2a6a55 100644 --- a/src/components/Canvas/DepthCanvas/components/layer-panel/layer-item.vue +++ b/src/components/Canvas/DepthCanvas/components/layer-panel/layer-item.vue @@ -1,33 +1,43 @@ @@ -42,6 +52,10 @@ layer: { type: Object, default: () => ({}) + }, + isGroup: { + type: Boolean, + default: false } }) const nameInputRef = ref(null) @@ -72,82 +86,98 @@ const info = props.layer.info layerManager.setLayerLockById(info.id, !info.lock) } + const onShowGroup = () => { + props.layer.info.showChildren = !props.layer.info.showChildren + // layerManager.setLayerGroupVisibleById(props.layer.info.id, !props.layer.visible) + } diff --git a/src/components/Canvas/DepthCanvas/manager/AISelectboxToolManager.ts b/src/components/Canvas/DepthCanvas/manager/AISelectboxToolManager.ts index 85f7b4b..ad09ee4 100644 --- a/src/components/Canvas/DepthCanvas/manager/AISelectboxToolManager.ts +++ b/src/components/Canvas/DepthCanvas/manager/AISelectboxToolManager.ts @@ -1,5 +1,6 @@ import { fabric } from 'fabric-with-all' -import { createId } from '../../tools/tools' +import { OperationType } from '../tools/layerHelper' +import { getObjectAlphaToCanvas, traceImageContour } from '../tools/canvasMethod' /** 智能框选工具管理器 */ export class AISelectboxToolManager { @@ -7,6 +8,7 @@ export class AISelectboxToolManager { canvasManager: any stateManager: any layerManager: any + toolManager: any isDragging: boolean = false startX: number = 0 @@ -16,7 +18,7 @@ export class AISelectboxToolManager { this.canvasManager = options.canvasManager this.stateManager = options.stateManager this.layerManager = options.layerManager - + this.toolManager = options.toolManager } mouseDownEvent(e) { this.isDragging = true @@ -81,15 +83,11 @@ export class AISelectboxToolManager { stroke: "rgba(255, 77, 71, 1)", strokeWidth: 1.5, strokeDashArray: [4, 4], - fill: "rgba(255, 186, 186, 0.5)", + fill: "transparent", strokeUniform: true, // 保持描边宽度不随缩放改变 - // strokeLineCap: "round",// 折线端点样式 - // strokeLineJoin: "bevel", // 折线连接样式 - // selectable: false, - // evented: false, - excludeFromExport: true, - hoverCursor: "default", - moveCursor: "default", + selectable: false, + evented: false, + absolutePositioned: true, }; async createSelectbox() { const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png" @@ -112,200 +110,31 @@ export class AISelectboxToolManager { }).join(" L "); const path = new fabric.Path(`M ${str} z`); path.set({ - left: left + minX * scaleX, - top: top + minY * scaleY, + left: left + minX, + top: top + minY, scaleX: scaleX, scaleY: scaleY, ...this.selectionStyle, }); - const rect1 = new fabric.Rect({ - left: 0, - top: 0, - width: 100, - height: 100, - fill: '#f00', - info: { - id: createId("rect"), - name: '矩形图层', - } - }) - const rect2 = new fabric.Rect({ - left: 200, - top: 200, - width: 100, - height: 100, - fill: '#ff0', - info: { - id: createId("rect"), - name: '矩形图层', - } - }) - this.layerManager.createGroupLayer({ - child: [rect1, rect2], - }) - // this.canvasManager.canvas.add(path) - // this.canvasManager.canvas.renderAll() - + const group = await this.layerManager.createGroupLayer({ + clipPath: path, + }, false, false) + const rect = await this.layerManager.createRectLayer({ + width: path.width, + height: path.height, + left: left + minX, + top: top + minY, + fill: "rgba(255, 186, 186, 0.5)", + info: { parentId: group.info.id }, + }, false, true) + await this.canvasManager.updateSubLayerClipPath() + await this.layerManager.updateLayerThumbnailsById(rect.info.id, "", false) + await this.layerManager.updateLayerThumbnailsById(group.info.id, rect.thumbnail) + this.stateManager.recordState() + this.toolManager.setTool(OperationType.SELECT) } - - - - dispose() { } } - - -/** - * 获取对象黑白通道画布 - * @param {fabric.Object} object - 要处理的 fabric 对象 - * @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明 - * @param {number} diff - 差值,默认 25 - * @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 } - * @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData - * @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败 -*/ -export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) { - const image = object.getElement(); - if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") { - console.warn("对象不是图片"); - return null; - } - const { width, height } = image; - if (!width || !height) { - console.warn("对象没有元素"); - return null; - } - const canvas = document.createElement("canvas"); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(image, 0, 0, width, height); - const data = ctx.getImageData(0, 0, width, height); - for (let i = 0; i < data.data.length; i += 4) { - const r = data.data[i + 0]; - const g = data.data[i + 1]; - const b = data.data[i + 2]; - const a = data.data[i + 3]; - const revR = revData?.data[i + 0] || 0; - const revG = revData?.data[i + 1] || 0; - const revB = revData?.data[i + 2] || 0; - const revA = revData?.data[i + 3] || 0; - let isHave = false; - if (r || g || b || a) { - if (revR > diff || revG > diff || revB > diff || revA > diff) { - isHave = false; - } else { - isHave = true; - } - } - if (isMerge && (revR || revG || revB || revA)) isHave = true; - if (isHave) { - data.data[i + 0] = rgba.r; - data.data[i + 1] = rgba.g; - data.data[i + 2] = rgba.b; - data.data[i + 3] = rgba.a; - } else { - data.data[i + 0] = 0; - data.data[i + 1] = 0; - data.data[i + 2] = 0; - data.data[i + 3] = 0; - } - } - ctx.clearRect(0, 0, width, height); - ctx.putImageData(data, 0, 0); - return canvas; -} - - -/** - * 图片边界跟踪算法(透明底) - * @param {HTMLCanvasElement} canvas - canvas元素 - * @param {Number} scale - 缩放比例 - * @returns {Array} 边界点数组 [{x, y}, ...] -*/ -export function traceImageContour(canvas) { - const ctx = canvas.getContext("2d", { willReadFrequently: true }); - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; - const width = canvas.width; - const height = canvas.height; - - // 查找起始点(第一个不透明像素) - let startX = -1; - let startY = -1; - - outer: for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const index = (y * width + x) * 4; - if (data[index + 3] > 0) { - startX = x; - startY = y; - break outer; - } - } - } - - if (startX === -1) return []; // 没有不透明像素 - - // Moore-Neighbor边界跟踪算法 - const contour = []; - const visited = new Set(); - const directions = [ - [-1, 0], - [-1, -1], - [0, -1], - [1, -1], - [1, 0], - [1, 1], - [0, 1], - [-1, 1], - ]; - - let currentX = startX; - let currentY = startY; - let backtrackDir = 4; // 起始方向:右 - - do { - const pointKey = `${currentX},${currentY}`; - if (!visited.has(pointKey)) { - contour.push({ x: currentX, y: currentY }); - visited.add(pointKey); - } - - // 从右方向开始顺时针查找 - let found = false; - for (let i = 0; i < 8; i++) { - const dir = (backtrackDir + i) % 8; - const dx = directions[dir][0]; - const dy = directions[dir][1]; - const checkX = currentX + dx; - const checkY = currentY + dy; - - if ( - checkX >= 0 && - checkX < width && - checkY >= 0 && - checkY < height - ) { - const index = (checkY * width + checkX) * 4; - if (data[index + 3] > 0) { - currentX = checkX; - currentY = checkY; - backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向 - found = true; - break; - } - } - } - - if (!found) break; - } while ( - !(currentX === startX && currentY === startY) && - visited.size < width * height - ); - - return contour; -} \ No newline at end of file diff --git a/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts b/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts index 2f8eb50..89553fc 100644 --- a/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts +++ b/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts @@ -5,11 +5,12 @@ import { AnimationManager } from './AnimationManager' import { detectDeviceType } from '../tools/index' import { CanvasEventManager } from "./events/CanvasEventManager"; import { OperationType } from '../tools/layerHelper' +import { cloneObjects } from '../tools/canvasMethod' import { createId } from '../../tools/tools' import md5 from 'md5' // 自定义画布转对象属性 -fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail"]; +fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail", "absolutePositioned"]; fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject fabric.Object.prototype.toObject = function () { const args = [...arguments] @@ -129,9 +130,12 @@ export class CanvasManager { /** 测试-开始 */ // this.stateManager.setIsRecord(false) - // const rect = await this.layerManager.createRectLayer({ left: 200 }) - // await this.layerManager.createStarLayer({ left: 400 }) - // await this.layerManager.createArrowLayer({ left: 600 }) + // const groupObject = await this.layerManager.createGroupLayer() + // const parentId = groupObject.info.id + // const rect = await this.layerManager.createRectLayer({ left: 200, info: { parentId } }) + // const star = await this.layerManager.createStarLayer({ left: 400, info: { parentId } }) + // const arrow = await this.layerManager.createArrowLayer({ left: 600, info: { parentId } }) + // await this.layerManager.createGroupLayer() // this.layerManager.setActiveID(rect.info.id) // this.stateManager.setIsRecord(true) /** 测试-结束 */ @@ -147,7 +151,7 @@ export class CanvasManager { this.canvas.add(obj) const id = obj?.info?.id || "" if (id) { - this.layerManager.updateLayers() + await this.layerManager.updateLayers(!!obj.info.parentId) this.renderAll() await this.layerManager.updateLayerThumbnailsById(id) } @@ -162,6 +166,35 @@ export class CanvasManager { } } + /** 更新子图层裁剪区域 */ + async updateSubLayerClipPath() { + const objects = this.getObjects().filter((v: any) => v.type !== "group" && !!v.info?.id); + for (let i = 0; i < objects.length; i++) { + let object = objects[i] + if (object.clipPath) object.set({ clipPath: null }) + let group = this.getObjectById(object.info.parentId) + if (!group) continue + let path = group.clipPath + if (!path) continue + let clipPath = await cloneObjects([path]).then((v) => v[0]) + clipPath.set({ + absolutePositioned: true, + }) + object.set({ clipPath }) + } + this.renderAll() + } + /** 排序画布对象 */ + async sortObjectByIds(ids: string[], isRecord?: boolean) { + ids.forEach((id, index) => { + this.canvas.moveTo(this.getObjectById(id), index) + }) + await this.updateSubLayerClipPath() + this.renderAll() + if (isRecord) this.stateManager.recordState() + } + + /** 设置画布事件 */ setupCanvasEvents() { // 创建画布事件管理器 @@ -177,8 +210,18 @@ export class CanvasManager { } /** 设置激活对象 */ setActiveObjectById(id: string) { + this.discardActiveObject() const obj = this.getObjectById(id) - if (obj && obj.evented) this.canvas.setActiveObject(obj) + if (!obj) return + if (obj.type === "group") { + const objects = []; + this.getObjects().forEach((item: any) => { + if (item?.info?.parentId === id) objects.push(item) + }) + if (objects.length > 0) this.canvas.setActiveObject(new fabric.ActiveSelection(objects, { canvas: this.canvas })); + } else { + if (obj.evented) this.canvas.setActiveObject(obj) + } this.renderAll() } resetZoom(animated = true, adaptive = true) { @@ -222,11 +265,11 @@ export class CanvasManager { renderAll() { this.canvas.renderAll() } - deleteObjectById(id: string) { + deleteObjectById(id: string, isUpdate = true) { const object = this.getObjectById(id) if (object) { this.canvas.remove(object) - this.layerManager.updateLayers() + if (isUpdate) this.layerManager.updateLayers() this.renderAll() } } @@ -235,13 +278,6 @@ export class CanvasManager { this.canvas.discardActiveObject() this.renderAll() } - // 拖拽排序 - dragSort(id, newIndex) { - this.canvas.moveTo(this.getObjectById(id), newIndex) - this.layerManager.updateLayers() - this.renderAll() - this.stateManager.recordState() - } /** 画笔事件 */ setupBrushEvents() { @@ -254,13 +290,13 @@ export class CanvasManager { }; } /** 处理绘制图像 */ - handleDrawImage(fabricImage: fabric.Object) { + async handleDrawImage(fabricImage: fabric.Object) { const activeID = this.stateManager.layerManager.activeID.value const activeLayer = this.getObjectById(activeID) if (activeLayer) { this.layerManager.imageMergeToLayer(activeLayer, fabricImage) } else { - const emptyLayer = this.layerManager.createEmptyLayer(false); + const emptyLayer = await this.layerManager.createEmptyLayer(false); this.layerManager.setActiveID(emptyLayer.info.id, false) this.layerManager.imageMergeToLayer(emptyLayer, fabricImage) } diff --git a/src/components/Canvas/DepthCanvas/manager/LayerManager.ts b/src/components/Canvas/DepthCanvas/manager/LayerManager.ts index aabe849..8199631 100644 --- a/src/components/Canvas/DepthCanvas/manager/LayerManager.ts +++ b/src/components/Canvas/DepthCanvas/manager/LayerManager.ts @@ -28,7 +28,18 @@ export class LayerManager { return this.getLayerById(this.activeID.value) } getLayerById(id) { - return this.layers.value.find((item: any) => item.info.id === id) + function call(arr) { + for (let i = 0; i < arr.length; i++) { + let v = arr[i] + if (v.info.id === id) return v + if (v.children) { + let layer = call(v.children) + if (layer) return layer + } + } + return null + } + return call(this.layers.value) } setLayerNameById(id, name: string) { const layer = this.getLayerById(id) @@ -82,6 +93,10 @@ export class LayerManager { /** 删除指定图层 */ deleteLayerById(id, isActive = true) { + const layer = this.getLayerById(id) + if (layer.children) { + layer.children.forEach(v => this.canvasManager.deleteObjectById(v.info.id, false)) + } this.canvasManager.deleteObjectById(id) if (id === this.activeID.value && isActive) { this.setActiveID(this.layers.value[0]?.info?.id || "") @@ -106,17 +121,38 @@ export class LayerManager { this.setActiveID(newObject.info.id) }) } - // 拖拽排序 - dragSort(id, newIndex) { - const index = Math.abs(this.layers.value.length - newIndex - 1) - this.canvasManager.dragSort(id, index) + /** 根据layers排序图层 */ + async sortLayers(isRecord?: boolean) { + const ids = []; + call(this.layers.value) + await this.canvasManager.sortObjectByIds(ids.reverse(), isRecord) + function call(arr) { + arr.forEach(v => { + ids.push(v.info.id) + if (v.children) call(v.children) + }) + } } + // 更新图层列表 - updateLayers() { - this.layers.value = this.canvasManager.getObjects() - .filter((v: any) => !!v?.info?.id) - .reverse() - .map(v => v.toObject()) + async updateLayers(isSort = false) { + const objects = this.canvasManager.getObjects().map(v => v.toObject()).filter(v => !!v.info?.id).reverse() + objects.forEach(v => { + if (v.type === "group") { + if (!v.children) v.children = [] + return; + } + const parentId = v.info?.parentId + if (!parentId) return + objects.forEach((obj: any) => { + if (obj.info?.id !== parentId) return + if (!obj.children) obj.children = [] + obj.children.push(v) + }) + }) + const layers = objects.filter(v => !v.info?.parentId) + this.layers.value = layers + if (isSort) await this.sortLayers() } /** 设置图层位置-不设置默认居中 */ @@ -136,7 +172,7 @@ export class LayerManager { } } /** 创建空图层 */ - createEmptyLayer(isRecord = true, isActive = false) { + async createEmptyLayer(isRecord = true, isActive = false) { const emptyObject = new fabric.Rect({ width: 0, height: 0, @@ -147,37 +183,28 @@ export class LayerManager { } }) this.setLayerPosition(emptyObject) - this.canvasManager.add(emptyObject, isRecord) + await this.canvasManager.add(emptyObject, isRecord) if (isActive) this.setActiveID(emptyObject.info.id, false) return emptyObject } /** 创建组图层 */ - createGroupLayer(options?: any, isRecord = true, isActive = false) { - const child = options?.child || [] - delete options.child - const groupObject = new fabric.Group(child, { - // subTargetCheck: true, // 关键:检测子对象 - // interactive: true, // 启用交互 - // hasControls: true, - // hasBorders: true, - - // // 子对象样式 - // cornerColor: 'blue', - // cornerSize: 8, - // borderColor: 'green', - - // // 允许子对象独立变换 - // lockScalingX: false, - // lockScalingY: false, - // lockRotation: false, + async createGroupLayer(options?: any, isRecord = true, isActive = false) { + const children = options?.children || [] + delete options.children + const groupObject = new fabric.Group(children, { + ...(options || {}), + hasControls: false, // 不显示控制点 + hasBorders: false, // 不显示边框 + selectable: false, // 不可选中(可选) info: { id: createId("group"), name: '组图层', + showChildren: true, ...(options?.info || {}), } }) - // this.setLayerPosition(groupObject) - this.canvasManager.add(groupObject, isRecord) + this.setLayerPosition(groupObject, options) + await this.canvasManager.add(groupObject, isRecord) if (isActive) this.setActiveID(groupObject.info.id, false) return groupObject } @@ -199,7 +226,7 @@ export class LayerManager { return textObject } /** 创建矩形图层 */ - async createRectLayer(options?: any, isActive = false) { + async createRectLayer(options?: any, isRecord = true, isActive = true) { const rectObject = new fabric.Rect({ width: 100, height: 100, @@ -213,12 +240,12 @@ export class LayerManager { } }) this.setLayerPosition(rectObject, options) - await this.canvasManager.add(rectObject) + await this.canvasManager.add(rectObject, isRecord) if (isActive) this.setActiveID(rectObject.info.id) return rectObject } /** 创建直线图层 */ - async createLineLayer(options?: any, isActive = false) { + async createLineLayer(options?: any, isRecord = true, isActive = true) { const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0] delete options.x1 delete options.y1 @@ -235,14 +262,13 @@ export class LayerManager { } }) this.setLayerPosition(lineObject, options) - await this.canvasManager.add(lineObject) + await this.canvasManager.add(lineObject, isRecord) if (isActive) this.setActiveID(lineObject.info.id) return lineObject } /** 创建椭圆图层 */ - async createEllipseLayer(options?: any, isActive = false) { + async createEllipseLayer(options?: any, isRecord = true, isActive = true) { const ellipseObject = new fabric.Ellipse({ - radius: 50, fill: '#000', strokeWidth: 0, ...(options || {}), @@ -258,7 +284,7 @@ export class LayerManager { return ellipseObject } /** 创建三角形图层 */ - async createTriangleLayer(options?: any, isActive = false) { + async createTriangleLayer(options?: any, isRecord = true, isActive = true) { const triangleObject = new fabric.Triangle({ width: 100, height: 100, @@ -272,12 +298,12 @@ export class LayerManager { } }) this.setLayerPosition(triangleObject, options) - await this.canvasManager.add(triangleObject) + await this.canvasManager.add(triangleObject, isRecord) if (isActive) this.setActiveID(triangleObject.info.id) return triangleObject } /** 创建五角星图层 */ - async createStarLayer(options?: any, isActive = false) { + async createStarLayer(options?: any, isRecord = true, isActive = true) { const width = options?.width || 100 const height = options?.height || 100 delete options.points @@ -292,12 +318,12 @@ export class LayerManager { } }) this.setLayerPosition(starObject, options) - await this.canvasManager.add(starObject) + await this.canvasManager.add(starObject, isRecord) if (isActive) this.setActiveID(starObject.info.id) return starObject } /** 创建箭头图层 */ - async createArrowLayer(options?: any, isActive = false) { + async createArrowLayer(options?: any, isRecord = true, isActive = true) { const width = options?.width || 100 const height = options?.height || 10 delete options.width @@ -316,7 +342,7 @@ export class LayerManager { } }); this.setLayerPosition(arrowObject, options) - this.canvasManager.add(arrowObject) + await this.canvasManager.add(arrowObject, isRecord) if (isActive) this.setActiveID(arrowObject.info.id) return arrowObject } @@ -324,7 +350,7 @@ export class LayerManager { /** 创建图片图层 */ - async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) { + async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true, isActive = true) { const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize(); const imageObject = await new Promise((resolve) => { @@ -350,7 +376,7 @@ export class LayerManager { }) as fabric.Object this.setLayerPosition(imageObject, options) await this.canvasManager.add(imageObject, isRecord) - this.setActiveID(imageObject.info.id) + if (isActive) this.setActiveID(imageObject.info.id) return imageObject } @@ -363,6 +389,7 @@ export class LayerManager { left: info.left, top: info.top, info: { + ...(targetLayer?.info || {}), id: createId("image"), name: targetLayer?.info?.name || "合并图层", } @@ -370,7 +397,6 @@ export class LayerManager { resolve(img) }, { crossOrigin: 'anonymous' }) }) - // console.log(mergedImage) const index = this.canvasManager.getObjects().indexOf(targetLayer); this.deleteLayerById(targetLayer.info.id, false) this.setActiveID(mergedImage.info.id, false) @@ -391,12 +417,12 @@ export class LayerManager { }) } /** 更新图层缩略图 */ - async updateLayerThumbnailsById(id: string) { + async updateLayerThumbnailsById(id: string, thumbnail?: string, isUpdate = true) { const object = this.canvasManager.getObjectById(id); if (!object) return; - const url = await exportObjectToThumbnail(object); + const url = thumbnail || await exportObjectToThumbnail(object); object.thumbnail = url - this.updateLayers() + if (isUpdate) this.updateLayers() } dispose() { } diff --git a/src/components/Canvas/DepthCanvas/manager/ShapeToolManager.ts b/src/components/Canvas/DepthCanvas/manager/ShapeToolManager.ts index 2699414..c6056cd 100644 --- a/src/components/Canvas/DepthCanvas/manager/ShapeToolManager.ts +++ b/src/components/Canvas/DepthCanvas/manager/ShapeToolManager.ts @@ -131,7 +131,7 @@ export class ShapeToolManager { upRectangle(object) { if (object.width === 0) object.width = 100 if (object.height === 0) object.height = 100 - this.layerManager.createRectLayer(object, true) + this.layerManager.createRectLayer(object) } /** 绘制直线 */ @@ -151,7 +151,7 @@ export class ShapeToolManager { }) } upLine(object) { - this.layerManager.createLineLayer(object, true) + this.layerManager.createLineLayer(object) } /** 绘制椭圆 */ @@ -170,7 +170,7 @@ export class ShapeToolManager { upEllipse(object) { if (object.rx === 0) object.rx = 50 if (object.ry === 0) object.ry = 50 - this.layerManager.createEllipseLayer(object, true) + this.layerManager.createEllipseLayer(object) } @@ -192,7 +192,7 @@ export class ShapeToolManager { upTriangle(object) { if (object.width === 0) object.width = 100 if (object.height === 0) object.height = 100 - this.layerManager.createTriangleLayer(object, true) + this.layerManager.createTriangleLayer(object) } @@ -217,7 +217,7 @@ export class ShapeToolManager { upStar(object) { if (object.width === 0) object.width = 100 if (object.height === 0) object.height = 100 - this.layerManager.createStarLayer(object, true) + this.layerManager.createStarLayer(object) } /** 绘制箭头 */ @@ -249,7 +249,7 @@ export class ShapeToolManager { top: this.startY, }, true) } else { - this.layerManager.createArrowLayer(object, true) + this.layerManager.createArrowLayer(object) } } diff --git a/src/components/Canvas/DepthCanvas/manager/StateManager.ts b/src/components/Canvas/DepthCanvas/manager/StateManager.ts index b68be75..f692dd3 100644 --- a/src/components/Canvas/DepthCanvas/manager/StateManager.ts +++ b/src/components/Canvas/DepthCanvas/manager/StateManager.ts @@ -71,6 +71,7 @@ export class StateManager { /** 记录状态 */ recordState() { if (this.running.value) return + console.log("recordState") this.running.value = true if (this.historyIndex.value < this.historyList.value.length - 1) { this.historyList.value.splice(this.historyIndex.value + 1) diff --git a/src/components/Canvas/DepthCanvas/manager/events/CanvasEventManager.js b/src/components/Canvas/DepthCanvas/manager/events/CanvasEventManager.js index 335a056..8f7230b 100644 --- a/src/components/Canvas/DepthCanvas/manager/events/CanvasEventManager.js +++ b/src/components/Canvas/DepthCanvas/manager/events/CanvasEventManager.js @@ -730,8 +730,10 @@ export class CanvasEventManager { }); this.canvas.on("object:modified", async (e) => { // updateLayers(e); - const id = e.target?.info?.id; + const target = e.target; + const id = target?.info?.id; if (id) await this.layerManager.updateLayerThumbnailsById(id) + if (target.type === "group") await this.canvasManager.updateSubLayerClipPath() this.stateManager.recordState(); }); this.canvas.on("object:removed", (e) => { diff --git a/src/components/Canvas/DepthCanvas/tools/canvasMethod.js b/src/components/Canvas/DepthCanvas/tools/canvasMethod.js index a506e8d..3792222 100644 --- a/src/components/Canvas/DepthCanvas/tools/canvasMethod.js +++ b/src/components/Canvas/DepthCanvas/tools/canvasMethod.js @@ -93,3 +93,157 @@ export function angleBetweenPointsDegrees(x1, y1, x2, y2) { return deg; } + + + +/** + * 获取对象黑白通道画布 + * @param {fabric.Object} object - 要处理的 fabric 对象 + * @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明 + * @param {number} diff - 差值,默认 25 + * @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 } + * @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData + * @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败 +*/ +export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) { + const image = object.getElement(); + if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") { + console.warn("对象不是图片"); + return null; + } + const { width, height } = image; + if (!width || !height) { + console.warn("对象没有元素"); + return null; + } + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + ctx.drawImage(image, 0, 0, width, height); + const data = ctx.getImageData(0, 0, width, height); + for (let i = 0; i < data.data.length; i += 4) { + const r = data.data[i + 0]; + const g = data.data[i + 1]; + const b = data.data[i + 2]; + const a = data.data[i + 3]; + const revR = revData?.data[i + 0] || 0; + const revG = revData?.data[i + 1] || 0; + const revB = revData?.data[i + 2] || 0; + const revA = revData?.data[i + 3] || 0; + let isHave = false; + if (r || g || b || a) { + if (revR > diff || revG > diff || revB > diff || revA > diff) { + isHave = false; + } else { + isHave = true; + } + } + if (isMerge && (revR || revG || revB || revA)) isHave = true; + if (isHave) { + data.data[i + 0] = rgba.r; + data.data[i + 1] = rgba.g; + data.data[i + 2] = rgba.b; + data.data[i + 3] = rgba.a; + } else { + data.data[i + 0] = 0; + data.data[i + 1] = 0; + data.data[i + 2] = 0; + data.data[i + 3] = 0; + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(data, 0, 0); + return canvas; +} + + +/** + * 图片边界跟踪算法(透明底) + * @param {HTMLCanvasElement} canvas - canvas元素 + * @param {Number} scale - 缩放比例 + * @returns {Array} 边界点数组 [{x, y}, ...] +*/ +export function traceImageContour(canvas) { + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + const width = canvas.width; + const height = canvas.height; + + // 查找起始点(第一个不透明像素) + let startX = -1; + let startY = -1; + + outer: for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + if (data[index + 3] > 0) { + startX = x; + startY = y; + break outer; + } + } + } + + if (startX === -1) return []; // 没有不透明像素 + + // Moore-Neighbor边界跟踪算法 + const contour = []; + const visited = new Set(); + const directions = [ + [-1, 0], + [-1, -1], + [0, -1], + [1, -1], + [1, 0], + [1, 1], + [0, 1], + [-1, 1], + ]; + + let currentX = startX; + let currentY = startY; + let backtrackDir = 4; // 起始方向:右 + + do { + const pointKey = `${currentX},${currentY}`; + if (!visited.has(pointKey)) { + contour.push({ x: currentX, y: currentY }); + visited.add(pointKey); + } + + // 从右方向开始顺时针查找 + let found = false; + for (let i = 0; i < 8; i++) { + const dir = (backtrackDir + i) % 8; + const dx = directions[dir][0]; + const dy = directions[dir][1]; + const checkX = currentX + dx; + const checkY = currentY + dy; + + if ( + checkX >= 0 && + checkX < width && + checkY >= 0 && + checkY < height + ) { + const index = (checkY * width + checkX) * 4; + if (data[index + 3] > 0) { + currentX = checkX; + currentY = checkY; + backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向 + found = true; + break; + } + } + } + + if (!found) break; + } while ( + !(currentX === startX && currentY === startY) && + visited.size < width * height + ); + + return contour; +} \ No newline at end of file diff --git a/src/components/Canvas/DepthCanvas/tools/exportMethod.js b/src/components/Canvas/DepthCanvas/tools/exportMethod.js index dbf897b..01cd8fa 100644 --- a/src/components/Canvas/DepthCanvas/tools/exportMethod.js +++ b/src/components/Canvas/DepthCanvas/tools/exportMethod.js @@ -29,6 +29,12 @@ export async function exportObjectsToImage(objects = [], isDetails = false) { left: obj.left - boundingBox.left, top: obj.top - boundingBox.top, }) + if (obj.clipPath && obj.clipPath.absolutePositioned) { + obj.clipPath.set({ + left: obj.clipPath.left - boundingBox.left, + top: obj.clipPath.top - boundingBox.top, + }) + } staticCanvas.add(obj) }) // 导出图片