feat: 添加导出图层功能,支持将图层转换为位图图像并下载
This commit is contained in:
1
src/assets/icons/CExport.svg
Normal file
1
src/assets/icons/CExport.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750664320391" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5213" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M459.6 515.7c0-33.2-26.9-60.1-60.1-60.1s-60.2 26.9-60.2 60.1 26.9 60.1 60.2 60.1c33.1 0.1 60.1-26.8 60.1-60.1zM585.2 776.1c15.7-98.2 94.8-175.1 194-187.5l-10-32.5-50.8 16.9-44.1-83L532 695.4l-76.2-49.2-119.5 129.9h248.9z" fill="#040000" p-id="5214"></path><path d="M259.7 829V398c0-7.7 6.2-13.9 13.9-13.9h607.1c7.7 0 13.9 6.2 13.9 13.9v206.4c23.5 9.9 44.9 23.6 63.6 40.4V351.9c0-17.4-14.1-31.4-31.4-31.4h-134l-79.3-206.2c-6.2-16.2-24.4-24.3-40.6-18.1L20.2 347.1C4 353.4-4.1 371.6 2.1 387.8l194 504.5v0.3h401.2c-7.7-20-12.6-41.3-14.3-63.6H259.7z m400.7-660c2.1-0.3 5.5-0.2 7.3 3.8l56.8 147.6H266.6L660.4 169zM196.1 715.1L77.9 407.9c-2.7-7.1 0.8-15.2 8-17.9l110.2-42.4v367.5z" fill="#040000" p-id="5215"></path><path d="M1022.5 800L871 678.5c-2.3-1.7-5.5-0.1-5.5 2.8v75.1H670c-5.3 0-9.7 4.3-9.7 9.6v75.7c0 5.3 4.3 9.7 9.7 9.7h195.5v75.1c0 2.8 3.3 4.5 5.5 2.8l151.5-121.5c2.6-2 2.6-5.9 0-7.8z" p-id="5216"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -10,6 +10,7 @@ import {
|
|||||||
insertObjectAtZIndex,
|
insertObjectAtZIndex,
|
||||||
} from "../utils/helper";
|
} from "../utils/helper";
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
|
import { imageModeHandler } from "../utils/imageHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置活动图层命令
|
* 设置活动图层命令
|
||||||
@@ -725,48 +726,13 @@ export class ChangeFixedImageCommand extends Command {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是包含 则需要根据图像模式调整大小
|
// 通用处理图片模式
|
||||||
switch (this.imageMode) {
|
imageModeHandler({
|
||||||
case "stretch":
|
imageMode: this.imageMode,
|
||||||
// 拉伸模式 - 填充整个画布
|
newImage,
|
||||||
newImage.scaleToWidth(this.canvasWidth);
|
canvasWidth: this.canvasWidth,
|
||||||
newImage.scaleToHeight(this.canvasHeight);
|
canvasHeight: 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位置插入新图像
|
// 使用帮助函数在指定z-index位置插入新图像
|
||||||
if (this.previousZIndex !== undefined && this.previousZIndex >= 0) {
|
if (this.previousZIndex !== undefined && this.previousZIndex >= 0) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
removeCanvasObjectByObject,
|
removeCanvasObjectByObject,
|
||||||
} from "../utils/helper";
|
} from "../utils/helper";
|
||||||
import { createRasterizedImage } from "../utils/rasterizedImage";
|
import { createRasterizedImage } from "../utils/rasterizedImage";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 栅格化图层命令
|
* 栅格化图层命令
|
||||||
@@ -376,3 +377,196 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出图层命令
|
||||||
|
* 将图层中的所有矢量对象转换为位图图像
|
||||||
|
* 支持普通图层和组图层的栅格化
|
||||||
|
*/
|
||||||
|
export class ExportLayerToImageCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: "导出图层",
|
||||||
|
saveState: true,
|
||||||
|
});
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layers = options.layers;
|
||||||
|
this.layerId = options.layerId; // 指定要栅格化的图层ID
|
||||||
|
// 是否包含锁定对象
|
||||||
|
this.hasLocked = options.hasLocked || true;
|
||||||
|
// 是否包含隐藏对象
|
||||||
|
this.hasHidden = options.hasHidden || false;
|
||||||
|
|
||||||
|
this.activeLayerId = options.activeLayerId;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
|
||||||
|
// 查找目标图层
|
||||||
|
const { layer, parent } = findLayerRecursively(
|
||||||
|
this.layers.value,
|
||||||
|
this.layerId
|
||||||
|
);
|
||||||
|
this.layer = layer;
|
||||||
|
this.parentLayer = parent;
|
||||||
|
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
||||||
|
|
||||||
|
// 保存原始状态用于撤销
|
||||||
|
this.originalLayers = [...this.layers.value];
|
||||||
|
this.originalCanvasObjects = [...this.canvas.getObjects()];
|
||||||
|
this.originalObjectStates = new Map();
|
||||||
|
|
||||||
|
// 栅格化结果
|
||||||
|
this.rasterizedImage = null;
|
||||||
|
this.rasterizedImageId = null;
|
||||||
|
// 生成新图层ID
|
||||||
|
this.rasterizedLayerId = generateId("rasterized_layer_");
|
||||||
|
this.resterizedId = generateId("rasterized_");
|
||||||
|
|
||||||
|
this.rasterizedLayer = null;
|
||||||
|
|
||||||
|
// 要栅格化的图层和对象
|
||||||
|
this.layersToRasterize = [];
|
||||||
|
this.objectsToRasterize = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
// 查找目标图层
|
||||||
|
const { layer, parent } = findLayerRecursively(
|
||||||
|
this.layers.value,
|
||||||
|
this.layerId
|
||||||
|
);
|
||||||
|
this.layer = layer;
|
||||||
|
this.parentLayer = parent;
|
||||||
|
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
||||||
|
|
||||||
|
if (!this.layer) {
|
||||||
|
throw new Error(`图层 ${this.layerId} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 收集要栅格化的图层和对象
|
||||||
|
this._collectLayersAndObjects();
|
||||||
|
|
||||||
|
if (this.objectsToRasterize.length === 0) {
|
||||||
|
message.error("图层没有内容可导出");
|
||||||
|
throw new Error("图层没有内容可导出");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存原始对象状态
|
||||||
|
this.canvas.discardActiveObject();
|
||||||
|
this.canvas.renderAll();
|
||||||
|
|
||||||
|
// 创建图像
|
||||||
|
const imageBase64 = await createRasterizedImage({
|
||||||
|
canvas: this.canvas,
|
||||||
|
fabricObjects: this.objectsToRasterize,
|
||||||
|
isReturenDataURL: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟浏览器下载
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = imageBase64;
|
||||||
|
link.download = `${this.layer.name}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
console.log(`✅ 图层 ${this.layer.name} 导出完成`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出图层失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async undo() {
|
||||||
|
// 导出图片不需要撤销操作
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集要栅格化的图层和对象
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_collectLayersAndObjects() {
|
||||||
|
if (this.isGroupLayer) {
|
||||||
|
// 组图层:收集自身和所有子图层
|
||||||
|
this.layersToRasterize = this._collectLayersToRasterize(this.layer);
|
||||||
|
} else {
|
||||||
|
// 普通图层:只收集自身
|
||||||
|
this.layersToRasterize = [this.layer];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有图层的fabricObjects并按画布z-index顺序排序
|
||||||
|
const allCanvasObjects = this.canvas.getObjects();
|
||||||
|
const objectsWithZIndex = [];
|
||||||
|
|
||||||
|
this.layersToRasterize.forEach((layer) => {
|
||||||
|
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||||
|
layer.fabricObjects.forEach((layerObj) => {
|
||||||
|
if (layerObj && layerObj.id) {
|
||||||
|
const { object } = findObjectById(this.canvas, layerObj.id);
|
||||||
|
if (object) {
|
||||||
|
// 获取对象在画布中的z-index(数组索引)
|
||||||
|
const zIndex = allCanvasObjects.indexOf(object);
|
||||||
|
objectsWithZIndex.push({
|
||||||
|
object: object,
|
||||||
|
zIndex: zIndex,
|
||||||
|
layerObj: layerObj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按z-index排序,确保保持原有的渲染顺序
|
||||||
|
objectsWithZIndex.sort((a, b) => a.zIndex - b.zIndex);
|
||||||
|
|
||||||
|
// 提取排序后的对象
|
||||||
|
this.objectsToRasterize = objectsWithZIndex.map((item) => item.object);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`📊 收集到 ${this.layersToRasterize.length} 个图层,${this.objectsToRasterize.length} 个对象进行栅格化`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"🔢 对象z-index顺序:",
|
||||||
|
objectsWithZIndex.map((item) => ({
|
||||||
|
id: item.object.id,
|
||||||
|
type: item.object.type,
|
||||||
|
zIndex: item.zIndex,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集要栅格化的图层(递归收集子图层)
|
||||||
|
* @param {Object} sourceLayer 源图层
|
||||||
|
* @returns {Array} 图层数组
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_collectLayersToRasterize(sourceLayer) {
|
||||||
|
const result = [sourceLayer];
|
||||||
|
|
||||||
|
// 如果是组图层,收集所有子图层
|
||||||
|
if (sourceLayer.children && sourceLayer.children.length > 0) {
|
||||||
|
sourceLayer.children.forEach((childLayer) => {
|
||||||
|
if (childLayer) {
|
||||||
|
result.push(...this._collectLayersToRasterize(childLayer));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfo() {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
originalLayerId: this.layerId,
|
||||||
|
originalLayerName: this.layer?.name,
|
||||||
|
rasterizedLayerId: this.rasterizedLayerId,
|
||||||
|
rasterizedLayerName: this.rasterizedLayer?.name,
|
||||||
|
isGroupLayer: this.isGroupLayer,
|
||||||
|
objectCount: this.objectsToRasterize?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
generateId,
|
generateId,
|
||||||
optimizeCanvasRendering,
|
optimizeCanvasRendering,
|
||||||
} from "../utils/helper.js";
|
} from "../utils/helper.js";
|
||||||
|
import { imageModeHandler } from "../utils/imageHelper.js";
|
||||||
import { LayerType, OperationType } from "../utils/layerHelper.js";
|
import { LayerType, OperationType } from "../utils/layerHelper.js";
|
||||||
import { Command, CompositeCommand } from "./Command.js";
|
import { Command, CompositeCommand } from "./Command.js";
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
@@ -26,6 +27,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
this.redGreenImageUrl = options.redGreenImageUrl;
|
this.redGreenImageUrl = options.redGreenImageUrl;
|
||||||
this.onImageGenerated = options.onImageGenerated;
|
this.onImageGenerated = options.onImageGenerated;
|
||||||
this.normalLayerOpacity = options.normalLayerOpacity || 0.4;
|
this.normalLayerOpacity = options.normalLayerOpacity || 0.4;
|
||||||
|
this.clothingImageOpts = options.clothingImageOpts || null; // 衣服底图选项 - 用于设置图片加载时的选项
|
||||||
|
|
||||||
// 存储原始状态以便撤销
|
// 存储原始状态以便撤销
|
||||||
this.originalCanvasBackground = null;
|
this.originalCanvasBackground = null;
|
||||||
@@ -398,15 +400,30 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
* 设置衣服底图
|
* 设置衣服底图
|
||||||
*/
|
*/
|
||||||
async _setupClothingImage(img, fixedLayer) {
|
async _setupClothingImage(img, fixedLayer) {
|
||||||
// 计算图片缩放,保持上下留边距
|
if (this.clothingImageOpts?.imageMode) {
|
||||||
const margin = 50;
|
// 如果有衣服底图选项,应用这些选项
|
||||||
const maxWidth = this.canvas.width - margin * 2;
|
// 底图加载方式 1.平铺 2.拉伸 3.拉伸平铺 4.拉伸平铺并裁剪 5.包含
|
||||||
const maxHeight = this.canvas.height - margin * 2;
|
// this.clothingImageOpts?.imageMode // 默认不处理 可选 contains, stretch,tile, stretchTile, stretchTileCrop
|
||||||
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
|
// 通用处理图片模式
|
||||||
|
imageModeHandler({
|
||||||
|
imageMode: this.clothingImageOpts?.imageMode,
|
||||||
|
newImage: img,
|
||||||
|
canvasWidth: this.canvas.width,
|
||||||
|
canvasHeight: this.canvas.height,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 计算图片缩放,保持上下留边距
|
||||||
|
const margin = 50;
|
||||||
|
const maxWidth = this.canvas.width - margin * 2;
|
||||||
|
const maxHeight = this.canvas.height - margin * 2;
|
||||||
|
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
|
||||||
|
img.set({
|
||||||
|
scaleX: scale,
|
||||||
|
scaleY: scale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
img.set({
|
img.set({
|
||||||
scaleX: scale,
|
|
||||||
scaleY: scale,
|
|
||||||
left: this.canvas.width / 2,
|
left: this.canvas.width / 2,
|
||||||
top: this.canvas.height / 2,
|
top: this.canvas.height / 2,
|
||||||
originX: "center",
|
originX: "center",
|
||||||
|
|||||||
@@ -665,7 +665,19 @@ function buildContextMenuItems(layer) {
|
|||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 栅格化图层
|
||||||
|
{
|
||||||
|
label: "导出图层",
|
||||||
|
icon: "CExport",
|
||||||
|
disabled:
|
||||||
|
layer.isBackground ||
|
||||||
|
layer.isFixed ||
|
||||||
|
!layerManager?.canRasterizeLayer?.(layer.id),
|
||||||
|
action: () => {
|
||||||
|
exportLayerToImage(layer.id);
|
||||||
|
hideContextMenu();
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: "divider" },
|
{ type: "divider" },
|
||||||
// 分组操作 - 带子菜单
|
// 分组操作 - 带子菜单
|
||||||
{
|
{
|
||||||
@@ -1107,6 +1119,25 @@ async function rasterizeLayer(layerId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出图层
|
||||||
|
async function exportLayerToImage(layerId) {
|
||||||
|
if (!layerManager?.rasterizeLayer) {
|
||||||
|
console.warn("导出图层功能不可用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await layerManager.exportLayerToImage(layerId);
|
||||||
|
if (success) {
|
||||||
|
console.log(`✅ 成功导出图层: ${layerId}`);
|
||||||
|
} else {
|
||||||
|
console.warn("导出图层失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出图层时发生错误:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 合并组图层
|
// 合并组图层
|
||||||
async function mergeGroupLayer(groupId) {
|
async function mergeGroupLayer(groupId) {
|
||||||
if (!layerManager?.mergeGroupLayers) {
|
if (!layerManager?.mergeGroupLayers) {
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { OperationType } from "../utils/layerHelper";
|
import { OperationType } from "../utils/layerHelper";
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
"tool-selected",
|
||||||
|
"trigger-image-upload",
|
||||||
|
"add-text",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
|
"toggle-minimap",
|
||||||
|
"zoom-in",
|
||||||
|
"zoom-out",
|
||||||
|
"toggle-red-green-mode",
|
||||||
|
"undo-redo-status-changed",
|
||||||
|
]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
activeTool: String,
|
activeTool: String,
|
||||||
minimapEnabled: {
|
minimapEnabled: {
|
||||||
@@ -24,24 +37,18 @@ const canRedo = ref(false);
|
|||||||
commandManager.setChangeCallback((info) => {
|
commandManager.setChangeCallback((info) => {
|
||||||
canUndo.value = info.canUndo;
|
canUndo.value = info.canUndo;
|
||||||
canRedo.value = info.canRedo;
|
canRedo.value = info.canRedo;
|
||||||
|
|
||||||
|
emit("undo-redo-status-changed", {
|
||||||
|
canUndo: canUndo.value,
|
||||||
|
canRedo: canRedo.value,
|
||||||
|
commandManager,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 撤销/重做操作
|
// 撤销/重做操作
|
||||||
const undoFun = () => commandManager.undo();
|
const undoFun = () => commandManager.undo();
|
||||||
const redoFun = () => commandManager.redo();
|
const redoFun = () => commandManager.redo();
|
||||||
|
|
||||||
const emit = defineEmits([
|
|
||||||
"tool-selected",
|
|
||||||
"trigger-image-upload",
|
|
||||||
"add-text",
|
|
||||||
"undo",
|
|
||||||
"redo",
|
|
||||||
"toggle-minimap",
|
|
||||||
"zoom-in",
|
|
||||||
"zoom-out",
|
|
||||||
"toggle-red-green-mode",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 普通模式工具列表
|
// 普通模式工具列表
|
||||||
const normalToolsList = ref([
|
const normalToolsList = ref([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const KeyboardShortcutHelp = defineAsyncComponent(() =>
|
|||||||
import("./components/KeyboardShortcutHelp.vue")
|
import("./components/KeyboardShortcutHelp.vue")
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits(["trigger-red-green-mouseup"]);
|
const emit = defineEmits(["trigger-red-green-mouseup", "changeCanvas"]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
canvasJSON: {
|
canvasJSON: {
|
||||||
@@ -296,6 +296,7 @@ onMounted(async () => {
|
|||||||
layerManager,
|
layerManager,
|
||||||
toolManager,
|
toolManager,
|
||||||
commandManager,
|
commandManager,
|
||||||
|
clothingImageOpts: props.clothingImageOpts,
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasManager.setRedGreenModeManager(redGreenModeManager);
|
canvasManager.setRedGreenModeManager(redGreenModeManager);
|
||||||
@@ -628,6 +629,15 @@ function handleChildLayersReorder(reorderData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理画布变更事件
|
||||||
|
const changeCanvas = (command) => {
|
||||||
|
const commandData = {
|
||||||
|
isChange: command.canUndo || command.canRedo, // 是否有可撤销或可重做的操作
|
||||||
|
...command, // 传递完整的命令数据
|
||||||
|
};
|
||||||
|
emit("changeCanvas", commandData);
|
||||||
|
};
|
||||||
|
|
||||||
// 提供外部ref实例方法
|
// 提供外部ref实例方法
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getCanvasManager: () => canvasManager, // 获取画布管理器实例
|
getCanvasManager: () => canvasManager, // 获取画布管理器实例
|
||||||
@@ -653,6 +663,7 @@ defineExpose({
|
|||||||
//图片url或者base64
|
//图片url或者base64
|
||||||
addImageToLayer: async (url) => {
|
addImageToLayer: async (url) => {
|
||||||
if (!url) return Promise.reject(new Error("图片URL不能为空"));
|
if (!url) return Promise.reject(new Error("图片URL不能为空"));
|
||||||
|
|
||||||
return await loadImageUrlToLayer({
|
return await loadImageUrlToLayer({
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
layerManager,
|
layerManager,
|
||||||
@@ -660,10 +671,6 @@ defineExpose({
|
|||||||
toolManager,
|
toolManager,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
//图片url或者base64数组 可选图层ID 不传默认新建图层
|
|
||||||
addMultipleImagesToLayer: (urls, layerId) => {
|
|
||||||
return canvasManager?.addMultipleImages(urls, layerId);
|
|
||||||
},
|
|
||||||
// 导出图片
|
// 导出图片
|
||||||
exportImage: ({
|
exportImage: ({
|
||||||
isContainBg = false, // 是否包含背景图层
|
isContainBg = false, // 是否包含背景图层
|
||||||
@@ -806,6 +813,7 @@ defineExpose({
|
|||||||
@add-text="handleAddText"
|
@add-text="handleAddText"
|
||||||
@zoom-in="zoomIn"
|
@zoom-in="zoomIn"
|
||||||
@zoom-out="zoomOut"
|
@zoom-out="zoomOut"
|
||||||
|
@undo-redo-status-changed="changeCanvas"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ import {
|
|||||||
BackgroundSizeWithScaleCommand,
|
BackgroundSizeWithScaleCommand,
|
||||||
} from "../commands/BackgroundCommands";
|
} from "../commands/BackgroundCommands";
|
||||||
import { MergeGroupLayerCommand } from "../commands/GroupCommands";
|
import { MergeGroupLayerCommand } from "../commands/GroupCommands";
|
||||||
import { RasterizeLayerCommand } from "../commands/RasterizeLayerCommand";
|
import {
|
||||||
|
ExportLayerToImageCommand,
|
||||||
|
RasterizeLayerCommand,
|
||||||
|
} from "../commands/RasterizeLayerCommand";
|
||||||
|
|
||||||
// 导入图层排序相关类和混入
|
// 导入图层排序相关类和混入
|
||||||
import {
|
import {
|
||||||
@@ -2865,6 +2868,57 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出图层 -- 下载图层图片
|
||||||
|
* @param {string} layerId 图层ID,默认使用当前活动图层
|
||||||
|
*/
|
||||||
|
async exportLayerToImage(layerId = null) {
|
||||||
|
const targetLayerId = layerId || this.activeLayerId.value;
|
||||||
|
|
||||||
|
if (!targetLayerId) {
|
||||||
|
console.warn($t("没有指定要栅格化的图层"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找目标图层
|
||||||
|
// const targetLayer = this.getLayerById(targetLayerId);
|
||||||
|
const { layer: targetLayer } = findLayerRecursively(
|
||||||
|
this.layers.value,
|
||||||
|
targetLayerId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetLayer) {
|
||||||
|
console.error($t("图层不存在", { layerId: targetLayerId }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接创建和执行导出命令
|
||||||
|
const command = new ExportLayerToImageCommand({
|
||||||
|
canvas: this.canvas,
|
||||||
|
layers: this.layers,
|
||||||
|
layerId: targetLayerId,
|
||||||
|
activeLayerId: this.activeLayerId,
|
||||||
|
layerManager: this,
|
||||||
|
});
|
||||||
|
|
||||||
|
command.undoable = false; // 导出操作通常不需要撤销
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
if (this.commandManager) {
|
||||||
|
const result = await this.commandManager.execute(command);
|
||||||
|
if (result) {
|
||||||
|
console.log(`✅ 成功导出图层: ${targetLayer.name}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const result = await command.execute();
|
||||||
|
if (result) {
|
||||||
|
console.log(`✅ 成功导出图层: ${targetLayer.name}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新图层缩略图
|
* 更新图层缩略图
|
||||||
* @param {string} layerId 图层ID
|
* @param {string} layerId 图层ID
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class RedGreenModeManager {
|
|||||||
// 图片URL
|
// 图片URL
|
||||||
this.clothingImageUrl = null;
|
this.clothingImageUrl = null;
|
||||||
this.redGreenImageUrl = null;
|
this.redGreenImageUrl = null;
|
||||||
|
this.clothingImageOpts = options.clothingImageOpts || null;
|
||||||
|
|
||||||
// 回调函数
|
// 回调函数
|
||||||
this.onImageGenerated = null;
|
this.onImageGenerated = null;
|
||||||
@@ -77,6 +78,7 @@ export class RedGreenModeManager {
|
|||||||
toolManager: this.toolManager,
|
toolManager: this.toolManager,
|
||||||
clothingImageUrl: this.clothingImageUrl,
|
clothingImageUrl: this.clothingImageUrl,
|
||||||
redGreenImageUrl: this.redGreenImageUrl,
|
redGreenImageUrl: this.redGreenImageUrl,
|
||||||
|
clothingImageOpts: this.clothingImageOpts,
|
||||||
normalLayerOpacity: this.normalLayerOpacity,
|
normalLayerOpacity: this.normalLayerOpacity,
|
||||||
onImageGenerated: this.onImageGenerated,
|
onImageGenerated: this.onImageGenerated,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2192,3 +2192,60 @@ function _getAlternativeMethods(analysis) {
|
|||||||
|
|
||||||
return alternatives;
|
return alternatives;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** * 图像模式处理函数
|
||||||
|
* 根据不同的图像模式调整图像大小和位置
|
||||||
|
* @param {Object} params - 参数对象
|
||||||
|
* @param {string} params.imageMode - 图像模式
|
||||||
|
* @param {fabric.Image} params.newImage - 新图像对象
|
||||||
|
* @param {number} params.canvasWidth - 画布宽度
|
||||||
|
* @param {number} params.canvasHeight - 画布高度
|
||||||
|
*/
|
||||||
|
export const imageModeHandler = ({
|
||||||
|
imageMode,
|
||||||
|
newImage,
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight,
|
||||||
|
}) => {
|
||||||
|
switch (imageMode) {
|
||||||
|
case "stretch":
|
||||||
|
// 拉伸模式 - 填充整个画布
|
||||||
|
newImage.scaleToWidth(canvasWidth);
|
||||||
|
newImage.scaleToHeight(canvasHeight);
|
||||||
|
break;
|
||||||
|
case "tile":
|
||||||
|
// 平铺模式 - 保持原始大小
|
||||||
|
newImage.scaleX = 1;
|
||||||
|
newImage.scaleY = 1;
|
||||||
|
break;
|
||||||
|
case "stretchTile":
|
||||||
|
// 拉伸平铺模式 - 填充整个画布,但保持宽高比
|
||||||
|
newImage.scaleToWidth(canvasWidth);
|
||||||
|
newImage.scaleToHeight(canvasHeight);
|
||||||
|
break;
|
||||||
|
case "stretchTileCrop":
|
||||||
|
// 拉伸平铺并裁剪模式 - 填充整个画布,可能
|
||||||
|
// 会裁剪图像以适应画布
|
||||||
|
newImage.scaleToWidth(canvasWidth);
|
||||||
|
newImage.scaleToHeight(canvasHeight);
|
||||||
|
// 这里可以添加裁剪逻辑,如果需要的话
|
||||||
|
// 例如使用fabric.Image.clipPath来裁剪图像
|
||||||
|
break;
|
||||||
|
case "contains":
|
||||||
|
// 包含模式 - 保证图像在画布内完整显示
|
||||||
|
// 既要考虑画布的宽高比,也要考虑图像的宽高比
|
||||||
|
// 图片缩放后要保证最长边能完全显示在画布内
|
||||||
|
const canvasAspect = canvasWidth / canvasHeight;
|
||||||
|
const imageAspect = newImage.width / newImage.height;
|
||||||
|
// 保证图像在画布内完整显示 - 既要考虑画布的宽高比,也要考虑图像的宽高比
|
||||||
|
// 图片缩放后要保证最长边能完全显示在画布内
|
||||||
|
if (imageAspect > canvasAspect) {
|
||||||
|
// 图像更宽
|
||||||
|
newImage.scaleToWidth(canvasWidth);
|
||||||
|
} else {
|
||||||
|
// 图像更高
|
||||||
|
newImage.scaleToHeight(canvasHeight);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user