diff --git a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js index 3459c6e9..c8112643 100644 --- a/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js +++ b/src/component/Canvas/CanvasEditor/commands/LassoCutoutCommand.js @@ -1,8 +1,7 @@ +import { createRasterizedImage } from "../utils/SelectionToImage.js"; import { CompositeCommand } from "./Command.js"; -import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js"; -import { ToolCommand } from "./ToolCommands.js"; +import { CreateImageLayerCommand } from "./LayerCommands.js"; import { ClearSelectionCommand } from "./SelectionCommands.js"; -import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js"; import { fabric } from "fabric-with-all"; /** @@ -27,7 +26,7 @@ export class LassoCutoutCommand extends CompositeCommand { this.executedCommands = []; // 高清截图选项 this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用 - this.baseResolutionScale = options.baseResolutionScale || 4; // 基础分辨率倍数,提高到4倍获得更清晰的图像 + this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数 } async execute() { @@ -48,18 +47,26 @@ export class LassoCutoutCommand extends CompositeCommand { // 确定源图层 const sourceLayer = this.layerManager.getActiveLayer(); - if (!sourceLayer || sourceLayer.fabricObjects.length === 0) { + if (!sourceLayer) { console.error("无法执行套索抠图:源图层无效"); return false; } + // 获取源图层的所有对象(包括子图层) + const sourceObjects = this._getLayerObjects(sourceLayer); + if (sourceObjects.length === 0) { + console.error("无法执行套索抠图:源图层没有可见对象"); + return false; + } + // 获取选区边界信息用于后续定位 const selectionBounds = selectionObject.getBoundingRect(true, true); - // 执行在当前画布上的抠图操作 - this.cutoutImageUrl = await this._performCutout( - sourceLayer, - selectionObject + // 使用createRasterizedImage执行抠图操作 + this.cutoutImageUrl = await this._performCutoutWithRasterized( + sourceObjects, + selectionObject, + selectionBounds ); if (!this.cutoutImageUrl) { console.error("抠图失败"); @@ -143,7 +150,120 @@ export class LassoCutoutCommand extends CompositeCommand { } /** - * 在当前画布上执行抠图操作 + * 获取图层的所有对象(包括子图层,从画布中查找真实对象) + * @param {Object} layer 图层对象 + * @returns {Array} 真实的fabric对象数组 + * @private + */ + _getLayerObjects(layer) { + const objects = []; + const canvasObjects = this.canvas.getObjects(); + + // 递归获取图层及其子图层的所有对象 + const collectLayerObjects = (currentLayer) => { + // 处理图层的fabricObjects + if ( + currentLayer.fabricObjects && + Array.isArray(currentLayer.fabricObjects) + ) { + currentLayer.fabricObjects.forEach((fabricRef) => { + if (fabricRef && fabricRef.id) { + // 从画布中查找真实的对象 + const realObject = canvasObjects.find( + (obj) => obj.id === fabricRef.id + ); + if (realObject && realObject.visible) { + objects.push(realObject); + } + } + }); + } + + // 处理单个fabricObject(背景图层等) + if (currentLayer.fabricObject && currentLayer.fabricObject.id) { + const realObject = canvasObjects.find( + (obj) => obj.id === currentLayer.fabricObject.id + ); + if (realObject && realObject.visible) { + objects.push(realObject); + } + } + + // 递归处理子图层 + if (currentLayer.children && Array.isArray(currentLayer.children)) { + currentLayer.children.forEach((childLayer) => { + if (childLayer.visible !== false) { + // 只处理可见的子图层 + collectLayerObjects(childLayer); + } + }); + } + }; + + collectLayerObjects(layer); + + console.log(`从图层 "${layer.name}" 收集到 ${objects.length} 个可见对象`); + return objects; + } + + /** + * 使用createRasterizedImage执行抠图操作 + * @param {Array} sourceObjects 源对象数组 + * @param {Object} selectionObject 选区对象 + * @param {Object} selectionBounds 选区边界 + * @returns {String} 抠图结果的DataURL + * @private + */ + async _performCutoutWithRasterized( + sourceObjects, + selectionObject, + selectionBounds + ) { + try { + console.log("=== 开始使用createRasterizedImage执行抠图 ==="); + console.log(`源对象数量: ${sourceObjects.length}`); + console.log(`选区边界:`, selectionBounds); + + // 确定缩放因子 + let scaleFactor = this.baseResolutionScale; + if (this.highResolutionEnabled) { + const devicePixelRatio = window.devicePixelRatio || 1; + scaleFactor = Math.max(scaleFactor, devicePixelRatio * 1.5); + } + + // 使用createRasterizedImage生成栅格化图像,将选区作为裁剪路径 + const rasterizedDataURL = await createRasterizedImage({ + canvas: this.canvas, + fabricObjects: sourceObjects, + clipPath: selectionObject, // 使用选区作为裁剪路径而不是遮罩 + trimWhitespace: true, + trimPadding: 2, + quality: 1.0, + format: "png", + scaleFactor: scaleFactor, + isReturenDataURL: true, // 返回DataURL + }); + + if (!rasterizedDataURL) { + throw new Error("栅格化生成图像失败"); + } + + console.log(`抠图完成,缩放因子: ${scaleFactor}x`); + return rasterizedDataURL; + } catch (error) { + console.error("使用createRasterizedImage执行抠图失败:", error); + + // 如果createRasterizedImage失败,回退到原始方法 + console.log("回退到原始抠图方法..."); + return await this._performCutout( + { fabricObjects: sourceObjects }, + selectionObject + ); + } + } + + /** + * 原始抠图方法(作为备用方案) * @param {Object} sourceLayer 源图层 * @param {Object} selectionObject 选区对象 * @returns {String} 抠图结果的DataURL @@ -151,13 +271,10 @@ export class LassoCutoutCommand extends CompositeCommand { */ async _performCutout(sourceLayer, selectionObject) { try { - console.log("=== 开始在当前画布执行抠图 ==="); + console.log("=== 使用原始方法执行抠图 ==="); // 获取选区边界 const selectionBounds = selectionObject.getBoundingRect(true, true); - console.log( - `选区边界: left=${selectionBounds.left}, top=${selectionBounds.top}, width=${selectionBounds.width}, height=${selectionBounds.height}` - ); // 保存画布当前状态 const originalActiveObject = this.canvas.getActiveObject(); @@ -175,7 +292,6 @@ export class LassoCutoutCommand extends CompositeCommand { const visibleObjects = sourceLayer.fabricObjects.filter( (obj) => obj.visible ); - console.log(`源图层可见对象数量: ${visibleObjects.length}`); if (visibleObjects.length === 0) { throw new Error("源图层没有可见对象"); @@ -184,36 +300,19 @@ export class LassoCutoutCommand extends CompositeCommand { // 如果只有一个对象且已经是组,直接使用 if (visibleObjects.length === 1 && visibleObjects[0].type === "group") { tempGroup = visibleObjects[0]; - console.log("使用现有组对象"); } else { // 创建临时组 - console.log("创建临时组..."); - - // 记录原始对象的位置和状态,用于后续恢复 - originalObjects = visibleObjects.map((obj) => ({ - object: obj, - originalLeft: obj.left, - originalTop: obj.top, - originalAngle: obj.angle, - originalScaleX: obj.scaleX, - originalScaleY: obj.scaleY, - })); - - // 不需要从画布移除原对象,直接创建组 - // 克隆对象来创建组,避免影响原对象 const clonedObjects = []; for (const obj of visibleObjects) { const cloned = await this._cloneObject(obj); clonedObjects.push(cloned); } - // 创建组 tempGroup = new fabric.Group(clonedObjects, { selectable: false, evented: false, }); - // 添加组到画布 this.canvas.add(tempGroup); } @@ -227,7 +326,6 @@ export class LassoCutoutCommand extends CompositeCommand { originY: "top", }); - // 应用裁剪路径到组 tempGroup.set({ clipPath: clipPath, }); @@ -242,23 +340,14 @@ export class LassoCutoutCommand extends CompositeCommand { height: selectionBounds.height, }; - // 设置高分辨率倍数,用于提高图像清晰度 + // 设置高分辨率倍数 let highResolutionScale = 1; - if (this.highResolutionEnabled) { - // 结合设备像素比和配置的基础倍数,确保在所有设备上都有最佳效果 const devicePixelRatio = window.devicePixelRatio || 1; - // 使用更激进的缩放策略,确保高清晰度 highResolutionScale = Math.max( this.baseResolutionScale, devicePixelRatio * 2 ); - - console.log( - `设备像素比: ${devicePixelRatio}, 基础倍数: ${this.baseResolutionScale}, 最终放大倍数: ${highResolutionScale}` - ); - } else { - console.log("高分辨率渲染已禁用,使用1x倍数"); } // 创建用于导出的高分辨率canvas @@ -269,7 +358,6 @@ export class LassoCutoutCommand extends CompositeCommand { colorSpace: "srgb", }); - // 设置canvas的实际像素尺寸(放大倍数) const actualWidth = Math.round( renderBounds.width * highResolutionScale ); @@ -279,99 +367,38 @@ export class LassoCutoutCommand extends CompositeCommand { exportCanvas.width = actualWidth; exportCanvas.height = actualHeight; - - // 设置canvas的显示尺寸(CSS尺寸,保持与选区一致) exportCanvas.style.width = renderBounds.width + "px"; exportCanvas.style.height = renderBounds.height + "px"; - // 启用最高质量渲染设置 exportCtx.imageSmoothingEnabled = true; exportCtx.imageSmoothingQuality = "high"; - - // 设置文本渲染质量 - if (exportCtx.textRenderingOptimization) { - exportCtx.textRenderingOptimization = "optimizeQuality"; - } - - // 设置线条和图形的渲染质量 - exportCtx.lineCap = "round"; - exportCtx.lineJoin = "round"; - exportCtx.miterLimit = 10; - - // 设置画布背景为透明 exportCtx.clearRect(0, 0, actualWidth, actualHeight); - // 获取画布当前的变换矩阵 const vpt = this.canvas.viewportTransform; const zoom = this.canvas.getZoom(); - // 保存当前变换状态 exportCtx.save(); - - // 应用高分辨率缩放 exportCtx.scale(highResolutionScale, highResolutionScale); - - // 应用偏移,只渲染选区部分 exportCtx.translate(-renderBounds.left, -renderBounds.top); - // 如果画布有缩放和平移,需要应用相应变换 if (zoom !== 1 || vpt[4] !== 0 || vpt[5] !== 0) { exportCtx.transform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]); } - // 渲染被裁剪的组 tempGroup.render(exportCtx); - exportCtx.restore(); - // 获取结果 - 使用最高质量设置 const dataUrl = exportCanvas.toDataURL("image/png", 1.0); - - console.log( - `抠图完成,选区尺寸: ${renderBounds.width}x${renderBounds.height}, 实际渲染尺寸: ${actualWidth}x${actualHeight}, 放大倍数: ${highResolutionScale}x` - ); - return dataUrl; } finally { // 清理和恢复 if (tempGroup) { - // 移除裁剪路径 tempGroup.set({ clipPath: null }); - - // 如果是我们创建的临时组,需要移除它 if (originalObjects.length > 0) { this.canvas.remove(tempGroup); - - // 恢复原始对象的状态(位置等信息保持不变) - originalObjects.forEach( - ({ - object, - originalLeft, - originalTop, - originalAngle, - originalScaleX, - originalScaleY, - }) => { - // 确保对象仍然在画布上且状态正确 - if (!this.canvas.getObjects().includes(object)) { - this.canvas.add(object); - } - - // 恢复原始变换状态(如果需要的话) - object.set({ - left: originalLeft, - top: originalTop, - angle: originalAngle, - scaleX: originalScaleX, - scaleY: originalScaleY, - }); - object.setCoords(); - } - ); } } - // 恢复画布状态 this.canvas.selection = originalSelection; if (originalActiveObject) { this.canvas.setActiveObject(originalActiveObject); @@ -379,7 +406,7 @@ export class LassoCutoutCommand extends CompositeCommand { this.canvas.renderAll(); } } catch (error) { - console.error("在当前画布执行抠图失败:", error); + console.error("原始方法执行抠图失败:", error); return null; } } @@ -401,39 +428,14 @@ export class LassoCutoutCommand extends CompositeCommand { return; } - // 计算画布中心位置 - const canvasCenter = this.canvas.getCenter(); + // 使用选区的位置作为图像位置 + let targetLeft = selectionBounds.left + selectionBounds.width / 2; + let targetTop = selectionBounds.top + selectionBounds.height / 2; - // 如果有选区边界信息,使用选区的原始位置和尺寸 - let targetLeft = canvasCenter.left; - let targetTop = canvasCenter.top; - let targetWidth = img.width; - let targetHeight = img.height; - - if (selectionBounds) { - // 使用选区的原始位置 - targetLeft = selectionBounds.left + selectionBounds.width / 2; - targetTop = selectionBounds.top + selectionBounds.height / 2; - - // 确保图像显示尺寸与选区尺寸一致 - targetWidth = selectionBounds.width; - targetHeight = selectionBounds.height; - - console.log( - `设置图像位置: left=${targetLeft}, top=${targetTop}, 尺寸: ${targetWidth}x${targetHeight}` - ); - } - - // 计算缩放比例以匹配目标尺寸 - const scaleX = targetWidth / img.width; - const scaleY = targetHeight / img.height; - - // 设置图像属性 + // 设置图像属性,保持选区的原始尺寸 img.set({ left: targetLeft, top: targetTop, - scaleX: scaleX, - scaleY: scaleY, originX: "center", originY: "center", selectable: true, @@ -455,6 +457,9 @@ export class LassoCutoutCommand extends CompositeCommand { // 更新坐标 img.setCoords(); + console.log( + `图像创建完成,位置: (${targetLeft}, ${targetTop}), 尺寸: ${img.width}x${img.height}` + ); resolve(img); }, { diff --git a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js index ad082a9a..da61fba7 100644 --- a/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/ObjectLayerCommands.js @@ -480,6 +480,12 @@ export class ChangeFixedImageCommand extends Command { this.position.x = options.left || this.canvas.width / 2; this.position.y = options.top || this.canvas.height / 2; + this.canvasWidth = options?.canvasWidth?.value || this.canvas.width; + this.canvasHeight = options?.canvasHeight?.value || this.canvas.height; + + // 底图加载方式 1.平铺 2.拉伸 3.拉伸平铺 4.拉伸平铺并裁剪 5.包含 + this.imageMode = options?.imageMode || ""; // 默认 contains, stretch,tile, stretchTile, stretchTileCrop + // 用于回滚的状态 this.previousImage = null; this.previousTransform = null; @@ -679,6 +685,7 @@ export class ChangeFixedImageCommand extends Command { async applyImageToLayer(newImage) { await optimizeCanvasRendering(this.canvas, async () => { // 设置基本属性 + newImage.set({ id: this.targetLayer?.fabricObject?.id || this.newObjectId, layerId: this.targetLayer.id, @@ -718,6 +725,49 @@ export class ChangeFixedImageCommand extends Command { }); } + // 如果是包含 则需要根据图像模式调整大小 + switch (this.imageMode) { + case "stretch": + // 拉伸模式 - 填充整个画布 + newImage.scaleToWidth(this.canvasWidth); + newImage.scaleToHeight(this.canvasHeight); + break; + case "tile": + // 平铺模式 - 保持原始大小 + newImage.scaleX = 1; + newImage.scaleY = 1; + break; + case "stretchTile": + // 拉伸平铺模式 - 填充整个画布,但保持宽高比 + newImage.scaleToWidth(this.canvasWidth); + newImage.scaleToHeight(this.canvasHeight); + break; + case "stretchTileCrop": + // 拉伸平铺并裁剪模式 - 填充整个画布,可能 + // 会裁剪图像以适应画布 + newImage.scaleToWidth(this.canvasWidth); + newImage.scaleToHeight(this.canvasHeight); + // 这里可以添加裁剪逻辑,如果需要的话 + // 例如使用fabric.Image.clipPath来裁剪图像 + break; + case "contains": + // 包含模式 - 保证图像在画布内完整显示 + // 既要考虑画布的宽高比,也要考虑图像的宽高比 + // 图片缩放后要保证最长边能完全显示在画布内 + const canvasAspect = this.canvasWidth / this.canvasHeight; + const imageAspect = newImage.width / newImage.height; + // 保证图像在画布内完整显示 - 既要考虑画布的宽高比,也要考虑图像的宽高比 + // 图片缩放后要保证最长边能完全显示在画布内 + if (imageAspect > canvasAspect) { + // 图像更宽 + newImage.scaleToWidth(this.canvasWidth); + } else { + // 图像更高 + newImage.scaleToHeight(this.canvasHeight); + } + break; + } + // 使用帮助函数在指定z-index位置插入新图像 if (this.previousZIndex !== undefined && this.previousZIndex >= 0) { const insertSuccess = insertObjectAtZIndex( diff --git a/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js b/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js index a6e41b98..186ffab9 100644 --- a/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/RedGreenCommands.js @@ -127,7 +127,10 @@ export class BatchInitializeRedGreenModeCommand extends Command { ); // 4. 确保背景图层大小和衣服地图大小一致 - await this._setupBackgroundLayer(backgroundLayer, this.clothingImage); + const backgroundObject = await this._setupBackgroundLayer( + backgroundLayer, + this.clothingImage + ); // 8. 设置普通图层透明度 this._setupNormalLayerOpacity(normalLayers); // 这里不需要在这里设置透明度 由图层统一处理 @@ -136,7 +139,7 @@ export class BatchInitializeRedGreenModeCommand extends Command { this.newEmptyLayerId = await this._createAndActivateEmptyLayer(); // 设置普通图层的裁剪对象为衣服底图 - if (this.redGreenImage) { + if (backgroundObject) { // const clipPathImg = this.redGreenImage; // clipPathImg.set({ // absolutePositioned: true, @@ -144,7 +147,7 @@ export class BatchInitializeRedGreenModeCommand extends Command { // 克隆衣服底图作为裁剪对象 this.redGreenImageMask = await new Promise((resolve, reject) => { - this.redGreenImage.clone((clonedImg) => { + backgroundObject.clone((clonedImg) => { if (!clonedImg) { reject(new Error("无法克隆红绿图")); return; @@ -366,6 +369,10 @@ export class BatchInitializeRedGreenModeCommand extends Command { fill: "transparent", // 确保背景是透明的 }); } + + object.setCoords(); + + return object; } /** diff --git a/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue b/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue index 6926638e..a26e584d 100644 --- a/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue +++ b/src/component/Canvas/CanvasEditor/components/HeaderMenu.vue @@ -25,6 +25,10 @@ const props = defineProps({ canvasColor: String, brushSize: Number, enabledRedGreenMode: Boolean, + showLayersPanel: { + type: Boolean, + default: true, // 是否显示图层面板 + }, }); const emit = defineEmits([ @@ -337,12 +341,11 @@ onMounted(() => { --> -