From 466d278b299962e667fb979301cbeda2df431ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E9=B9=8F?= <2916022834@qq.com> Date: Tue, 6 Jan 2026 14:17:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8D=B0=E8=8A=B1=E7=AD=89?= =?UTF-8?q?=E6=89=80=E6=9C=89=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/FillRepeatCommand.js | 4 + .../CanvasEditor/commands/LayerCommands.js | 44 ++-- .../components/LayersPanel/LayerItem.vue | 3 +- .../components/SelectMenuPanel/index.vue | 15 +- src/component/Canvas/CanvasEditor/index.vue | 10 + .../CanvasEditor/managers/CanvasManager.js | 212 +++++++++++++----- .../CanvasEditor/managers/ExportManager.js | 34 ++- .../CanvasEditor/managers/LayerManager.js | 17 +- .../Canvas/CanvasEditor/utils/helper.js | 20 ++ .../Canvas/CanvasEditor/utils/layerUtils.js | 1 + .../CanvasEditor/utils/selectionToImage.js | 31 ++- src/component/Canvas/canvasExample.vue | 21 +- 12 files changed, 301 insertions(+), 111 deletions(-) diff --git a/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js b/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js index 923cddf9..a866c299 100644 --- a/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js +++ b/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js @@ -80,6 +80,8 @@ export class FillRepeatCommand extends Command { source: FillSourceToBase64(img), gapX: 0, gapY: 0, + width: img.width, + height: img.height, }; const bgObject = this.canvasManager.getBackgroundLayerObject(); const pattern = new fabric.Pattern({ @@ -275,6 +277,8 @@ export class FillRepeatGapChangeCommand extends Command { const image = new Image(); image.src = object.fill_.source; await image.decode(); + object.fill_.width = image.width; + object.fill_.height = image.height; // 创建透明 Canvas const tcanvas = document.createElement('canvas'); tcanvas.width = image.width + object.fill_.gapX; diff --git a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js index 5f6335c3..0aa04417 100644 --- a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js @@ -524,6 +524,7 @@ export class RemoveLayerCommand extends Command { this.layerId = options.layerId; this.activeLayerId = options.activeLayerId; this.layerManager = options.layerManager || null; + this.IsOnlyLayer = this.layers.value.filter((v => !v.isFixed && !v.isFixedOther && !v.isBackground)).length <= 1 // 查找要删除的图层 this.layerIndex = this.layers.value.findIndex( @@ -600,7 +601,9 @@ export class RemoveLayerCommand extends Command { ); // 从图层列表中删除 this.layers.value.splice(this.layerIndex, 1); - + if(this.IsOnlyLayer){ + this.addCmd = await this.layerManager?.createLayer?.(null, LayerType.EMPTY, {}, false); + } // 如果删除的是当前活动图层,需要更新活动图层 if (this.isActiveLayer) { // 查找最近的非背景层作为新的活动图层 @@ -633,6 +636,9 @@ export class RemoveLayerCommand extends Command { async undo() { // 恢复图层到原位置 if (this.layerIndex !== -1 && this.removedLayer) { + if(this.IsOnlyLayer && this.addCmd){ + this.addCmd?.undo?.(); + } this.layers.value.splice(this.layerIndex, 0, this.removedLayer); // 使用优化渲染批处理恢复真实对象到画布 @@ -650,7 +656,6 @@ export class RemoveLayerCommand extends Command { } }); }); - await this.layerManager?.updateLayersObjectsInteractivity?.(); this.canvas.renderAll(); @@ -4286,24 +4291,28 @@ export class RemoveChildLayerCommand extends Command { } // 恢复子图层到原位置 this.parentLayer.children.splice(this.childIndex, 0, this.removedChild); - optimizeCanvasRendering(this.canvas, async () => { - this.originalObjects.forEach((obj) => { - // 恢复对象到画布 - this.canvas.add(obj); - // 恢复对象的图层信息 - obj.layerId = this.layerId; - obj.layerName = this.removedChild.name; - obj.setCoords(); // 更新坐标 - }); + await new Promise((resolve) => { + optimizeCanvasRendering(this.canvas, async () => { + this.originalObjects.forEach((obj) => { + // 恢复对象到画布 + this.canvas.add(obj); + // 恢复对象的图层信息 + obj.layerId = this.layerId; + obj.layerName = this.removedChild.name; + obj.setCoords(); // 更新坐标 + }); - // 如果是原活动图层,恢复活动图层 - if (this.isActiveLayer) { - this.activeLayerId.value = this.layerId; - } + // 如果是原活动图层,恢复活动图层 + if (this.isActiveLayer) { + this.activeLayerId.value = this.layerId; + } - // 重新渲染画布 - await this.layerManager?.updateLayersObjectsInteractivity(false); + // 重新渲染画布 + await this.layerManager?.updateLayersObjectsInteractivity(false); + resolve(true); + }); }); + return true; } getInfo() { @@ -4501,6 +4510,7 @@ export class SetColorLayerFillCommand extends Command { this.oldFill = JSON.parse(JSON.stringify(this.object.fill)); this.layer.blendMode = "multiply"; this.object.set("globalCompositeOperation", "multiply"); + this.object.set("originColor", options.originColor); } async execute(isUndo = false) { diff --git a/src/component/Canvas/CanvasEditor/components/LayersPanel/LayerItem.vue b/src/component/Canvas/CanvasEditor/components/LayersPanel/LayerItem.vue index 79bff827..b9c9cf13 100644 --- a/src/component/Canvas/CanvasEditor/components/LayersPanel/LayerItem.vue +++ b/src/component/Canvas/CanvasEditor/components/LayersPanel/LayerItem.vue @@ -364,14 +364,13 @@ const clickColor = () => { const fill = layerObject.value.fill; if (fill) { const obj = fillToPallet(fill); - console.log("===========:", obj); palletPanel(obj).then((res) => { - console.log("===========:", res); const cmd = new SetColorLayerFillCommand({ canvas: canvasManager.canvas, layerManager: layerManager, object: layerObject.value, newFill: palletToFill(res), + originColor: res, }); layerManager.commandManager.execute(cmd); }); diff --git a/src/component/Canvas/CanvasEditor/components/SelectMenuPanel/index.vue b/src/component/Canvas/CanvasEditor/components/SelectMenuPanel/index.vue index b4e5d3a9..5617c4e9 100644 --- a/src/component/Canvas/CanvasEditor/components/SelectMenuPanel/index.vue +++ b/src/component/Canvas/CanvasEditor/components/SelectMenuPanel/index.vue @@ -264,7 +264,7 @@ * 显示面板 */ function show() { - if (activeObjects.length === 0) return; + if (activeObjects.value.length === 0) return; visible.value = true; closePanel.value = true; } @@ -276,14 +276,14 @@ visible.value = false; } // 获取当前选中的对象 - const activeObjects = reactive([]); + const activeObjects = ref([]); const getActiveObject = (e) => { console.log("==========切换激活对象", e, activeObjects); - activeObjects.splice(0, activeObjects.length, ...e.selected); - activeObjects.forEach((v) => { + activeObjects.value = [...e.selected]; + activeObjects.value.forEach((v) => { v.layer = props.layerManager.getLayerById(v.layerId); }); - if (activeObjects.length === 0) { + if (activeObjects.value.length === 0) { close(); } else { show(); @@ -291,7 +291,7 @@ }; //取消当前选中 const cancelSelect = () => { - activeObjects.splice(0, activeObjects.length); + activeObjects.value = []; close(); }; const lastSelectLayerId = inject("lastSelectLayerId"); @@ -537,7 +537,7 @@ // 更新选中对象属性 const updateActiveObjects = (arrs, keys, isNumber = true) => { arrs.forEach((v) => { - activeObjects.forEach((item) => { + activeObjects.value.forEach((item) => { if (item.id === v.id) { keys.forEach( (key) => (item[key] = isNumber ? Number(v[key]) : v[key]) @@ -545,6 +545,7 @@ } }); }); + activeObjects.value = [...activeObjects.value]; }; // 旋转对象时更新角度 diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index 641c7e73..710d63df 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -1009,6 +1009,7 @@ defineExpose({ exportImage: ({ isContainBg = false, // 是否包含背景图层 isContainFixed = false, // 是否包含固定图层 + isContainFixedOther = false, // 是否包含其他固定图层 isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true,则导出时裁剪到背景图层大小 layerId = "", // 导出具体图层ID layerIdArray = [], // 导出多个图层ID数组 @@ -1018,6 +1019,7 @@ defineExpose({ return canvasManager.exportImage({ isContainBg, isContainFixed, + isContainFixedOther, isCropByBg, layerId, layerIdArray, @@ -1047,6 +1049,14 @@ defineExpose({ return result; }, + /** + * 导出所有信息 + * @returns {Object} 包含所有图层信息的对象 + */ + exportAllInfo: () => { + return canvasManager.exportAllInfo(); + }, + /** * 拖拽排序图层 * @param {number} oldIndex 原索引 diff --git a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js index 5bec6c3f..c8220b5d 100644 --- a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js +++ b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js @@ -30,7 +30,9 @@ import { palletToFill, fillToCssStyle, calculateRotatedTopLeftDeg, + calculateCenterPoint, createPatternTransform, + getTransformScaleAngle, base64ToCanvas, } from "../utils/helper"; import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands"; @@ -566,10 +568,10 @@ export class CanvasManager { } // 更新颜色层信息 - // const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); - // if(colorObject){ - // await this.setObjecCliptInfo(colorObject); - // } + const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); + if(colorObject){ + await this.setObjecCliptInfo(colorObject); + } const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP); if(groupLayer){ const groupRect = new fabric.Rect({}); @@ -808,6 +810,13 @@ export class CanvasManager { return layerObjectByLayerId; } + getObjectsByIds(ids){ + const objects = this.canvas.getObjects().filter((obj) => { + return ids.includes(obj.id); + }); + return objects; + } + /** * 更新蒙层位置 * @param {Object} backgroundLayerObject 背景层对象 @@ -936,6 +945,7 @@ export class CanvasManager { options.restoreOpacityInRedGreen !== undefined ? options.restoreOpacityInRedGreen : false, // 默认在红绿图模式下恢复透明度 + excludedLayers: [SpecialLayerId.SPECIAL_GROUP], }; // 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层 @@ -966,6 +976,22 @@ export class CanvasManager { } } + /** + * 导出所有信息 + * @returns {Object} 包含所有图层信息的对象 + */ + async exportAllInfo() { + // 导出颜色图层信息 + const color = await this.exportColorLayer().catch(() => (null)); + // 导出印花和元素图层信息 + const printTrimsData = await this.exportPrintTrimsLayers().catch(() => ({prints: null, trims: null})); + + return { + color, + ...printTrimsData, + }; + } + /** * 导出颜色图层 * @returns {Object} 导出的颜色图层数据URL @@ -980,13 +1006,12 @@ export class CanvasManager { console.warn("颜色图层不存在,请确保已添加颜色图层"); return Promise.reject("颜色图层不存在"); } - const color = fillToCssStyle(object.fill) + const css = fillToCssStyle(object.fill) const canvas = new fabric.StaticCanvas(); canvas.setDimensions({ width: object.width, height: object.height, backgroundColor: null, - // enableRetinaScaling: true, imageSmoothingEnabled: true, }); const cloneObject = await new Promise((resolve, reject) => { @@ -1007,9 +1032,75 @@ export class CanvasManager { quality: 1, }); canvas.clear(); - return {color, base64}; + const color = object.originColor; + return {css, base64, color}; } + /** + * 导出印花和元素图层 + */ + async exportPrintTrimsLayers() { + const object = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP); + if(!object) return Promise.reject("印花和元素图层组不存在"); + const ids = object.children.map((v) => v.id); + const objects = this.getObjectsByIds(ids).filter((v) => !!v.sourceData); + const fixedLayerObj = this.getFixedLayerObject(); + if(!fixedLayerObj) return Promise.reject("固定图层不存在"); + const flWidth = fixedLayerObj.width + const flHeight = fixedLayerObj.height + const flTop = fixedLayerObj.top + const flLeft = fixedLayerObj.left + const flScaleX = fixedLayerObj.scaleX + const flScaleY = fixedLayerObj.scaleY + const prints = []; + const trims = []; + objects.forEach((v) => { + const obj = { + ifSingle: v.sourceData.ifSingle, + level2Type: v.sourceData.level2Type, + designType: v.sourceData.designType, + path: v.sourceData.path, + location: [0, 0], + scale: [0, 0], + angle: v.angle, + name: v.sourceData.name, + } + if(obj.ifSingle){ + let left = (v.left - (flLeft - flWidth * flScaleX / 2)); + let top = (v.top - (flTop - flHeight * flScaleY / 2)); + let width = (v.width * v.scaleX); + let height = (v.height * v.scaleY); + let {x:cx, y:cy} = calculateCenterPoint(width, height, left, top, v.angle); + let x = (cx-width/2) / flScaleX; + let y = (cy-height/2) / flScaleY; + obj.location = [x, y]; + obj.scale = [(v.width * v.scaleX) / (flWidth * flScaleX), (v.height * v.scaleY) / (flHeight * flScaleY)]; + }else{ + let fill = v.fill; + let fill_ = v.fill_; + if(!fill || !fill_) return; + let {scale, angle} = getTransformScaleAngle(fill.patternTransform); + let scaleX = scale * 5 * v.fill_.width / flWidth; + let scaleY = scale * 5 * v.fill_.height / flHeight; + let scaleXY = flWidth > flHeight ? scaleX : scaleY; + + let left = fill.offsetX + v.fill_.width * scale / 2; + let top = fill.offsetY + v.fill_.height * scale / 2; + + obj.scale = [scaleXY, scaleXY]; + obj.angle = angle; + obj.location = [left, top]; + } + if(obj.level2Type === "Pattern"){ + prints.push(obj); + }else if(obj.level2Type === "Embroidery"){ + trims.push(obj); + } + }) + return {prints, trims}; + } + + dispose() { // 释放导出管理器资源 if (this.exportManager) { @@ -1126,6 +1217,12 @@ export class CanvasManager { "erasable", "customType", "fill_", + "scaleX", + "scaleY", + "top", + "left", + "width", + "height", ]), layers: simplifyLayersData, // 简化图层数据 // layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据 @@ -1300,9 +1397,9 @@ export class CanvasManager { singleLayers.unshift({...print}); } }) - otherData_?.trims?.prints?.forEach((print, index) => { - print.name = t("Canvas.Elements") + (index + 1); - printTrimsLayers.unshift({...print}); + otherData_?.trims?.prints?.forEach((trims, index) => { + trims.name = t("Canvas.Elements") + (index + 1); + printTrimsLayers.unshift({...trims}); }) await this.createPrintTrimsLayers(printTrimsLayers, singleLayers); } @@ -1350,7 +1447,6 @@ export class CanvasManager { if(!color) return console.warn("颜色为空不需要添加"); if(findLayer(this.layers.value, SpecialLayerId.COLOR)) return console.warn("画布中已存在颜色图层"); console.log("==========添加颜色图层", color, this.layers.value.length) - const fixedLayerObj = this.getFixedLayerObject(); // 创建颜色图层对象 const colorRect = new fabric.Rect({ id: SpecialLayerId.COLOR, @@ -1359,8 +1455,9 @@ export class CanvasManager { isVisible: true, isLocked: true, globalCompositeOperation: BlendMode.MULTIPLY, + originColor: color, }); - // await this.setObjecCliptInfo(colorRect); + await this.setObjecCliptInfo(colorRect); const gradientObj = palletToFill(color); const gradient = new fabric.Gradient({ type: 'linear', @@ -1398,24 +1495,24 @@ export class CanvasManager { const children = []; // 添加印花和元素图层 for(let index = 0; index < printTrimsLayers.length; index++){ - let print = printTrimsLayers[index]; + let item = printTrimsLayers[index]; let id = generateId("layer_image_"); - let name = print.name; + let name = item.name; let image = await new Promise(resolve => { - fabric.Image.fromURL(print.path, (fabricImage)=>{ - const left = flLeft - flWidth * flScaleX / 2 + (print.location?.[0] || 0) * flScaleX - const top = flTop - flHeight * flScaleY / 2 + (print.location?.[1] || 0) * flScaleY - const scaleX = flWidth * (print.scale?.[0] || 1) / fabricImage.width * flScaleX - const scaleY = flHeight * (print.scale?.[1] || 1) / fabricImage.height * flScaleY + fabric.Image.fromURL(item.path, (fabricImage)=>{ + const left = flLeft - flWidth * flScaleX / 2 + (item.location?.[0] || 0) * flScaleX + const top = flTop - flHeight * flScaleY / 2 + (item.location?.[1] || 0) * flScaleY + const scaleX = flWidth * (item.scale?.[0] || 1) / fabricImage.width * flScaleX + const scaleY = flHeight * (item.scale?.[1] || 1) / fabricImage.height * flScaleY const {x, y} = calculateRotatedTopLeftDeg( fabricImage.width * scaleX, fabricImage.height * scaleY, left, top, 0, - print.angle || 0 + item.angle || 0 ) - const angle = print.angle || 0 + const angle = item.angle || 0 fabricImage.set({ left: x, top: y, @@ -1428,6 +1525,7 @@ export class CanvasManager { selectable: true, hasControls: true, hasBorders: true, + sourceData: item, }); resolve(fabricImage); }, { crossOrigin: "anonymous" }); @@ -1446,11 +1544,11 @@ export class CanvasManager { }; // 添加平铺图层 for(let index = 0; index < singleLayers.length; index++){ - let print = singleLayers[index]; + let item = singleLayers[index]; let id = generateId("layer_image_"); - let name = print.name; + let name = item.name; let image = await new Promise(resolve => { - fabric.Image.fromURL(print.path, (fabricImage)=>{ + fabric.Image.fromURL(item.path, (fabricImage)=>{ const imgElement = fabricImage.getElement(); const tcanvas = document.createElement('canvas'); tcanvas.width = imgElement.width; @@ -1461,12 +1559,11 @@ export class CanvasManager { resolve(tcanvas); }, { crossOrigin: "anonymous" }); }) - console.log("==========添加平铺图层", fixedLayerObj.width,image.width) - let scaleX = fixedLayerObj.width / image.width * (print.scale?.[0] || 1) / 5; - let scaleY = fixedLayerObj.height / image.height * (print.scale?.[1] || 1) / 5; + let scaleX = fixedLayerObj.width / image.width * (item.scale?.[0] || 1) / 5; + let scaleY = fixedLayerObj.height / image.height * (item.scale?.[1] || 1) / 5; let scale = fixedLayerObj.width > fixedLayerObj.height ? scaleX : scaleY; - let left = (print.location?.[0] || 0) - image.width * scale / 2 - let top = (print.location?.[1] || 0) - image.height * scale / 2 + let left = (item.location?.[0] || 0) - image.width * scale / 2 + let top = (item.location?.[1] || 0) - image.height * scale / 2 let rect = new fabric.Rect({ id: id, layerId: id, @@ -1479,13 +1576,21 @@ export class CanvasManager { scaleY: fixedLayerObj.scaleY, originX: fixedLayerObj.originX, originY: fixedLayerObj.originY, + sourceData: item, fill: new fabric.Pattern({ source: image, repeat: "repeat", - patternTransform: createPatternTransform(scale, print.angle || 0), + patternTransform: createPatternTransform(scale, item.angle || 0), offsetX: left, // 水平偏移 offsetY: top, // 垂直偏移 }), + fill_ : { + source: item.path, + gapX: 0, + gapY: 0, + width: image.width, + height: image.height, + } }); this.canvas.add(rect); let layer = createLayer({ @@ -1531,31 +1636,32 @@ export class CanvasManager { } /** - * + * 画布事件变更后 */ async changeCanvas(){ - const fixedLayerObj = this.getFixedLayerObject(); - if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj) - const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); - if(colorObject){ - const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP); - if(ids.length === 0){ - ids.unshift(SpecialLayerId.SPECIAL_GROUP); - await this.setObjecCliptInfo(colorObject); - return; - } - const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true}); - if(!base64) return console.warn("导出图片失败", base64) - const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true); - const ctx = canvas.getContext('2d'); - const width = fixedLayerObj.width; - const height = fixedLayerObj.height; - const x = (canvas.width - width) / 2; - const y = (canvas.height - height) / 2; - const data = ctx.getImageData(x, y, width, height); - await this.setObjecCliptInfo(colorObject, data); - this.canvas.renderAll(); - } + // const fixedLayerObj = this.getFixedLayerObject(); + // if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj) + // const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); + // if(colorObject){ + // const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP); + // if(ids.length === 0){ + // ids.unshift(SpecialLayerId.SPECIAL_GROUP); + // await this.setObjecCliptInfo(colorObject); + // this.canvas.renderAll(); + // return; + // } + // const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true}); + // if(!base64) return console.warn("导出图片失败", base64) + // const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true); + // const ctx = canvas.getContext('2d'); + // const width = fixedLayerObj.width; + // const height = fixedLayerObj.height; + // const x = (canvas.width - width) / 2; + // const y = (canvas.height - height) / 2; + // const data = ctx.getImageData(x, y, width, height); + // await this.setObjecCliptInfo(colorObject, data); + // this.canvas.renderAll(); + // } } /** diff --git a/src/component/Canvas/CanvasEditor/managers/ExportManager.js b/src/component/Canvas/CanvasEditor/managers/ExportManager.js index 0db49fb1..9c41437c 100644 --- a/src/component/Canvas/CanvasEditor/managers/ExportManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ExportManager.js @@ -1,7 +1,7 @@ import { fabric } from "fabric-with-all"; import { findObjectById } from "../utils/helper"; import { createRasterizedImage } from "../utils/selectionToImage"; - import { OperationType, SpecialLayerId } from "../utils/layerHelper"; +import { OperationType, SpecialLayerId } from "../utils/layerHelper"; /** * 图片导出管理器 @@ -26,7 +26,8 @@ export class ExportManager { * @param {Array} options.layerIdArray2 导出多个图层ID数组2 * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 - * @param {Boolean} options.isEnhanceImg 是否是增强图片 + * @param {Boolean} options.isEnhanceImg 是否是增强图片 + * @param {Array} options.excludedLayers 排除的图层ID数组 * @returns {String} 导出的图片数据URL */ async exportImage(options = {}) { @@ -41,6 +42,7 @@ export class ExportManager { expPicType = "png", restoreOpacityInRedGreen = true, isEnhanceImg, // 是否是增强图片 + excludedLayers = [], // 排除的图层ID数组 } = options; try { // 查找颜色图层并隐藏 @@ -90,6 +92,7 @@ export class ExportManager { isCropByBg, isEnhanceImg, // 是否是增强图片 layerIdArray2, + excludedLayers, // 排除的图层ID数组 ); } catch (error) { console.error("导出图片失败:", error); @@ -240,7 +243,8 @@ export class ExportManager { restoreOpacityInRedGreen, isCropByBg, // 是否使用背景大小裁剪 isEnhanceImg, // 是否是增强图片 - layerIdArray, + layerIdArray, // 导出所有图层 + excludedLayers, // 排除的图层ID数组 ) { // 按图层顺序收集对象(从底到顶) const objectsToExport = this._collectObjectsByLayerOrder( @@ -248,6 +252,7 @@ export class ExportManager { isContainBg, isContainFixed, isContainFixedOther, // 是否包含其他固定图层 + excludedLayers, ); if (objectsToExport.length === 0) { @@ -303,10 +308,11 @@ export class ExportManager { /** * 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象) * @param {Object} layer 图层对象 + * @param {Boolean} isChildren 是否递归收集子图层的对象 * @returns {Array} 画布中的真实对象数组 * @private */ - _collectObjectsFromLayer(layer) { + _collectObjectsFromLayer(layer, isChildren = true) { if (!layer) { return []; } @@ -335,10 +341,10 @@ export class ExportManager { } // 递归收集子图层的对象 - if (layer.children && layer.children.length > 0) { + if (isChildren && layer.children && layer.children.length > 0) { for (let i = layer.children.length - 1; i >= 0; i--) { const childLayer = layer.children[i]; - const childObjects = this._collectObjectsFromLayer(childLayer); + const childObjects = this._collectObjectsFromLayer(childLayer, isChildren); realObjects.push(...childObjects); } } @@ -405,12 +411,13 @@ export class ExportManager { * @param {Boolean} isContainBg 是否包含背景图层 * @param {Boolean} isContainFixed 是否包含固定图层 * @param {Boolean} isContainFixedOther 是否包含其他固定图层 + * @param {Array} excludedLayers 排除的图层ID数组 * @returns {Array} 按正确顺序排列的真实对象数组 * @private */ - _collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther) { + _collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther, excludedLayers) { const objectsToExport = []; - const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表 + const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表 // 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序 for (let i = allLayers.length - 1; i >= 0; i--) { @@ -424,7 +431,7 @@ export class ExportManager { continue; if (layer.visible) { - const layerObjects = this._collectObjectsFromLayer(layer); + const layerObjects = this._collectObjectsFromLayer(layer, false); objectsToExport.push(...layerObjects); } } @@ -433,15 +440,19 @@ export class ExportManager { } /** - * 获取扁平化的图层列表(包含子图层) + * 获取扁平化的图层列表(包含子图层),排除指定的图层 + * @param {Array} excludedLayers 排除的图层ID数组 * @returns {Array} 扁平化的图层数组 * @private */ - _getAllLayersFlattened() { + _getAllLayersFlattened(excludedLayers) { const flattenedLayers = []; const rootLayers = this._getAllLayers(); const flattenLayer = (layer) => { + // 检查是否在排除列表中 + if (excludedLayers && excludedLayers.includes(layer.id)) return; + flattenedLayers.push(layer); // 递归处理子图层 @@ -456,7 +467,6 @@ export class ExportManager { for (const layer of rootLayers) { flattenLayer(layer); } - return flattenedLayers; } diff --git a/src/component/Canvas/CanvasEditor/managers/LayerManager.js b/src/component/Canvas/CanvasEditor/managers/LayerManager.js index 8f353d31..2e105cb8 100644 --- a/src/component/Canvas/CanvasEditor/managers/LayerManager.js +++ b/src/component/Canvas/CanvasEditor/managers/LayerManager.js @@ -524,15 +524,16 @@ export class LayerManager { * @param {string} name 图层名称 * @param {string} type 图层类型 * @param {Object} options 额外选项 + * @param {boolean} isCmd 是否创建命令 * @returns {string} 新创建的图层ID */ - async createLayer(name = null, type = LayerType.EMPTY, options = {}) { + async createLayer(name = null, type = LayerType.EMPTY, options = {}, isCmd = true) { // 生成唯一ID const layerId = options.id || options.layerId || generateId("layer_"); // 计算普通图层数量(非背景、非固定) const normalLayersCount = this.layers.value.filter( - (layer) => !layer.isBackground && !layer.isFixed + (layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther ).length; // 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置 // 添加到图层列表 @@ -544,7 +545,7 @@ export class LayerManager { // 创建新图层 const newLayer = createLayer({ id: layerId, - name: name || `图层 ${normalLayersCount + 1}`, + name: name || this.t("Canvas.EmptyLayer"), type: type, visible: true, locked: false, @@ -573,13 +574,13 @@ export class LayerManager { } // 执行命令 - if (this.commandManager) { + if (isCmd && this.commandManager) { await this.commandManager.execute(command); - } else { + } else{ await command.execute(); } - return layerId; + return isCmd ? layerId : command; } /** @@ -973,7 +974,7 @@ export class LayerManager { }) // const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther); console.log("普通图层:", normalLayers) - if (isChild ? parentLength <= 1 : normalLayers.length <= 1) { + if (isChild ? parentLength <= 1 : false) {//normalLayers.length <= 1 console.warn("不能删除唯一的普通图层"); message.warning(this.t("Canvas.cannotDeleteOnlyLayer")); return false; @@ -3446,7 +3447,7 @@ export class LayerManager { this.layers.value.forEach(layer => { if(layer.id === SpecialLayerId.SPECIAL_GROUP){ layer.children.forEach(child => { - if(child.blendMode && child.blendMode !== BlendMode.NORMAL){ + if(child.visible && child.blendMode && child.blendMode !== BlendMode.NORMAL){ blendModeLayerIds.push(child.id); } }); diff --git a/src/component/Canvas/CanvasEditor/utils/helper.js b/src/component/Canvas/CanvasEditor/utils/helper.js index 20a8a614..5a7fd912 100644 --- a/src/component/Canvas/CanvasEditor/utils/helper.js +++ b/src/component/Canvas/CanvasEditor/utils/helper.js @@ -919,6 +919,26 @@ export function calculateRotatedTopLeftDeg( return { x: newX, y: newY }; } +/** + * 根据左上角坐标计算中心点坐标 + * @param {number} W - 宽度 + * @param {number} H - 高度 + * @param {number} currentX - 当前左上角x坐标 + * @param {number} currentY - 当前左上角y坐标 + * @param {number} currentAngleDeg - 当前角度(度) + * @returns {Object} 中心点坐标 {x, y} + */ +export function calculateCenterPoint(W, H, currentX, currentY, currentAngleDeg) { + const currentAngle = (currentAngleDeg * Math.PI) / 180; + const cosCurrent = Math.cos(currentAngle); + const sinCurrent = Math.sin(currentAngle); + const Cx = currentX + (W / 2) * cosCurrent - (H / 2) * sinCurrent; + const Cy = currentY + (W / 2) * sinCurrent + (H / 2) * cosCurrent; + return { x: Cx, y: Cy }; +} + + + /** * 创建缩放+旋转的变换矩阵 * @param {number} scale - 缩放比例 diff --git a/src/component/Canvas/CanvasEditor/utils/layerUtils.js b/src/component/Canvas/CanvasEditor/utils/layerUtils.js index 1643eb8d..57600af7 100644 --- a/src/component/Canvas/CanvasEditor/utils/layerUtils.js +++ b/src/component/Canvas/CanvasEditor/utils/layerUtils.js @@ -211,6 +211,7 @@ export function simplifyLayers(layers) { fill: layer?.fill || null, fillColor: layer.fillColor, selectObject: layer.selectObject, + blendMode: layer.blendMode || null, }; return simplifiedLayer; diff --git a/src/component/Canvas/CanvasEditor/utils/selectionToImage.js b/src/component/Canvas/CanvasEditor/utils/selectionToImage.js index b1f019ef..75e39218 100644 --- a/src/component/Canvas/CanvasEditor/utils/selectionToImage.js +++ b/src/component/Canvas/CanvasEditor/utils/selectionToImage.js @@ -1,6 +1,6 @@ // 栅格化帮助 import { fabric } from "fabric-with-all"; - +import { SpecialLayerId } from "./layerHelper"; /** * 创建栅格化图像 - 重构版本 * 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置 @@ -691,6 +691,16 @@ const cloneObjectAsync = (obj) => { return new Promise((resolve, reject) => { obj.clone((cloned) => { if (cloned) { + cloned.set({ + scaleX: obj.scaleX, + scaleY: obj.scaleY, + top: obj.top, + left: obj.left, + width: obj.width, + height: obj.height, + zoomX: obj.zoomX, + zoomY: obj.zoomY, + }) resolve(cloned); } else { reject(new Error("对象克隆失败")); @@ -845,9 +855,8 @@ const renderContentToImage = async ({ }); // 克隆并添加所有需要渲染的对象 - for (const obj of fabricObjects) { - const clonedObj = await cloneObjectAsync(obj); - + for (let obj of fabricObjects) { + let clonedObj = await cloneObjectAsync(obj); // 调整对象位置:将选区左上角作为新的原点(0,0) clonedObj.set({ left: (clonedObj.left - selectionBounds.left) * qualityMultiplier, @@ -859,19 +868,19 @@ const renderContentToImage = async ({ }); // 如果有裁剪路径,也需要调整裁剪路径 - if (clonedObj.clipPath) { + if (clonedObj.clipPath && obj.id !== SpecialLayerId.COLOR) { clonedObj.clipPath.set({ - left: - (clonedObj.clipPath.left - selectionBounds.left) * - qualityMultiplier, - top: - (clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier, + left: (clonedObj.clipPath.left - selectionBounds.left) * qualityMultiplier, + top: (clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier, scaleX: (clonedObj.clipPath.scaleX || 1) * qualityMultiplier, scaleY: (clonedObj.clipPath.scaleY || 1) * qualityMultiplier, }); clonedObj.clipPath.setCoords(); // 更新裁剪路径坐标 } - + // if(obj.globalCompositeOperation === "multiply"){ + // clonedObj.clipPath = null; + // } + console.log("==========", obj.id, obj.layerName); contentCanvas.add(clonedObj); } diff --git a/src/component/Canvas/canvasExample.vue b/src/component/Canvas/canvasExample.vue index de88a03a..87a555f1 100644 --- a/src/component/Canvas/canvasExample.vue +++ b/src/component/Canvas/canvasExample.vue @@ -72,7 +72,9 @@ const exportImage = async () => { if (canvasEditor.value) { const base64 = await canvasEditor.value.exportImage({ isContainFixed: false, // 是否导出底图 + isContainFixedOther: false, // 是否导出其他固定图层 isContainBg: false, // 是否导出背景 + isEnhanceImg: false, // 是否导出增强图片 }); // 模拟下载图片 @@ -99,6 +101,16 @@ const exportColorLayer = async () => { } }; +// 导出所有信息 +const exportAllInfo = async () => { + if (canvasEditor.value) { + const allInfo = await canvasEditor.value.exportAllInfo(); + console.log("==========导出所有信息:", allInfo); + } +}; + + + const changeCanvas = (command) => { console.log(command); }; @@ -194,12 +206,19 @@ const frontBackChange =(value)=>{ // 自定义工具配置相关 const customToolsList = ref([ { - id: "exportPNG", + id: "exportColorLayer", title: "导出颜色图层", action: exportColorLayer, label: "导颜", class: "export-btn", }, + { + id: "exportAllInfo", + title: "导出所有信息", + action: exportAllInfo, + label: "导All", + class: "export-btn", + }, { id: "exportPNG", title: "导出PNG", //导出画布图片