合并画布
This commit is contained in:
10
components.d.ts
vendored
10
components.d.ts
vendored
@@ -9,9 +9,14 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||
@@ -20,9 +25,14 @@ declare module 'vue' {
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@@ -212,6 +212,10 @@ export class BackgroundSizeCommand extends Command {
|
||||
this.canvas.setWidth(this.newWidth);
|
||||
this.canvas.setHeight(this.newHeight);
|
||||
|
||||
console.log(
|
||||
`调整画布大小:${this.oldWidth}x${this.oldHeight} -> ${this.newWidth}x${this.newHeight}`
|
||||
);
|
||||
|
||||
// 如果使用 CanvasManager,通知它画布大小变化
|
||||
if (
|
||||
this.canvasManager &&
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createLayer,
|
||||
findLayerRecursively,
|
||||
LayerType,
|
||||
OperationType,
|
||||
} from "../utils/layerHelper";
|
||||
import { Command } from "./Command";
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
optimizeCanvasRendering,
|
||||
} from "../utils/helper";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { rasterizeCanvasObjects } from "../utils/imageHelper";
|
||||
|
||||
/**
|
||||
* 合并组图层命令 - 将图层及其子图层合并为单个图层
|
||||
@@ -30,7 +32,9 @@ export class MergeGroupLayerCommand extends Command {
|
||||
this.originalLayers = [...this.layers.value];
|
||||
this.originalObjects = [...this.canvas.getObjects()];
|
||||
this.flattenedLayer = null;
|
||||
this.flattenedLayerId = null;
|
||||
this.flattenedLayerId =
|
||||
generateId("flattened_") ||
|
||||
`flattened_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
this.existingGroupId = null; // 用于查找现有组对象
|
||||
// 组对象相关
|
||||
@@ -150,11 +154,6 @@ export class MergeGroupLayerCommand extends Command {
|
||||
console.log("单个对象,已更新关联关系");
|
||||
}
|
||||
|
||||
// 生成新图层ID
|
||||
this.flattenedLayerId = `flattened_${Date.now()}_${Math.floor(
|
||||
Math.random() * 1000
|
||||
)}`;
|
||||
|
||||
// 创建展平后的图层
|
||||
this.flattenedLayer = createLayer({
|
||||
id: this.flattenedLayerId,
|
||||
@@ -190,12 +189,14 @@ export class MergeGroupLayerCommand extends Command {
|
||||
this.activeLayerId.value = this.flattenedLayerId;
|
||||
|
||||
// 重新渲染画布
|
||||
await optimizeCanvasRendering(this.canvas, () => {
|
||||
this.canvas.renderAll();
|
||||
});
|
||||
// await optimizeCanvasRendering(this.canvas, () => {
|
||||
// this.canvas.renderAll();
|
||||
// });
|
||||
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
|
||||
this.canvas?.thumbnailManager?.generateLayerThumbnail?.(this.newGroupId);
|
||||
|
||||
console.log(`已合并图层:${this.flattenedLayer.name}`);
|
||||
return this.flattenedLayerId;
|
||||
}
|
||||
@@ -530,558 +531,3 @@ export class MergeGroupLayerCommand extends Command {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化图层命令
|
||||
* 将图层中的所有矢量对象转换为位图图像
|
||||
* 支持普通图层和组图层的栅格化
|
||||
*/
|
||||
export class RasterizeLayerCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "栅格化图层",
|
||||
saveState: true,
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.layerId = options.layerId;
|
||||
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;
|
||||
this.rasterizedLayerId = null;
|
||||
this.rasterizedLayer = null;
|
||||
|
||||
// 要栅格化的图层和对象
|
||||
this.layersToRasterize = [];
|
||||
this.objectsToRasterize = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (!this.layer) {
|
||||
throw new Error(`图层 ${this.layerId} 不存在`);
|
||||
}
|
||||
|
||||
// 检查是否可以栅格化
|
||||
if (this.layer.isBackground || this.layer.isFixed) {
|
||||
throw new Error("背景图层和固定图层不能栅格化");
|
||||
}
|
||||
|
||||
try {
|
||||
// 收集要栅格化的图层和对象
|
||||
this._collectLayersAndObjects();
|
||||
|
||||
if (this.objectsToRasterize.length === 0) {
|
||||
throw new Error("图层没有内容可栅格化");
|
||||
}
|
||||
|
||||
// 保存原始对象状态
|
||||
this._saveOriginalObjectStates();
|
||||
|
||||
// 创建栅格化图像
|
||||
const rasterizedImage = await this._createRasterizedImage();
|
||||
|
||||
// 创建新的栅格化图层并替换原图层
|
||||
await this._createRasterizedLayer(rasterizedImage);
|
||||
|
||||
console.log(`✅ 图层 ${this.layer.name} 栅格化完成`);
|
||||
return this.rasterizedLayerId;
|
||||
} catch (error) {
|
||||
console.error("栅格化图层失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (!this.originalLayers || !this.originalCanvasObjects) {
|
||||
throw new Error("没有可恢复的原始数据");
|
||||
}
|
||||
|
||||
try {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 清空画布
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.clear();
|
||||
|
||||
// 恢复原始对象及其状态
|
||||
this.originalCanvasObjects.forEach((obj) => {
|
||||
// 如果保存了该对象的原始状态,则恢复状态
|
||||
if (this.originalObjectStates.has(obj.id)) {
|
||||
const originalState = this.originalObjectStates.get(obj.id);
|
||||
obj.set(originalState);
|
||||
}
|
||||
|
||||
this.canvas.add(obj);
|
||||
obj.setCoords();
|
||||
});
|
||||
|
||||
// 恢复原始图层结构
|
||||
this.layers.value = [...this.originalLayers];
|
||||
|
||||
// 恢复原活动图层
|
||||
this.activeLayerId.value = this.layerId;
|
||||
|
||||
// 更新画布交互性
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
});
|
||||
|
||||
console.log(`↩️ 图层 ${this.layer.name} 栅格化已撤销`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("撤销栅格化失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集要栅格化的图层和对象
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存原始对象状态
|
||||
* @private
|
||||
*/
|
||||
_saveOriginalObjectStates() {
|
||||
this.objectsToRasterize.forEach((object) => {
|
||||
if (object && object.id) {
|
||||
const originalState = {
|
||||
left: object.left,
|
||||
top: object.top,
|
||||
scaleX: object.scaleX,
|
||||
scaleY: object.scaleY,
|
||||
angle: object.angle,
|
||||
flipX: object.flipX,
|
||||
flipY: object.flipY,
|
||||
opacity: object.opacity,
|
||||
originX: object.originX,
|
||||
originY: object.originY,
|
||||
layerId: object.layerId,
|
||||
layerName: object.layerName,
|
||||
width: object.width,
|
||||
height: object.height,
|
||||
strokeWidth: object.strokeWidth,
|
||||
visible: object.visible,
|
||||
};
|
||||
this.originalObjectStates.set(object.id, originalState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建栅格化图像
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
async _createRasterizedImage() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 计算所有对象的总边界
|
||||
const bounds = this._calculateObjectsBounds(this.objectsToRasterize);
|
||||
if (!bounds) {
|
||||
reject(new Error("无法计算对象边界"));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🎯 栅格化边界信息:", bounds);
|
||||
|
||||
// 创建临时画布进行栅格化
|
||||
const tempCanvas = this._createTempCanvas(bounds);
|
||||
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
backgroundColor: "transparent",
|
||||
renderOnAddRemove: false,
|
||||
});
|
||||
|
||||
// 直接复制对象到临时画布,不使用临时组
|
||||
this._addObjectsToTempCanvas(
|
||||
tempFabricCanvas,
|
||||
this.objectsToRasterize,
|
||||
bounds
|
||||
);
|
||||
|
||||
// 强制渲染临时画布
|
||||
tempFabricCanvas.renderOnAddRemove = true;
|
||||
tempFabricCanvas.renderAll();
|
||||
|
||||
// 等待渲染完成后生成图像
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 生成高质量图像
|
||||
const dataUrl = tempFabricCanvas.toDataURL({
|
||||
format: "png",
|
||||
quality: 1.0,
|
||||
multiplier: 1, // 降低multiplier避免性能问题
|
||||
});
|
||||
|
||||
console.log("📷 栅格化图像生成完成,DataURL长度:", dataUrl.length);
|
||||
|
||||
// 创建fabric图像对象
|
||||
fabric.Image.fromURL(
|
||||
dataUrl,
|
||||
(img) => {
|
||||
// 设置图像属性,使用原始边界的中心点
|
||||
this.rasterizedImageId = generateId("rasterized_");
|
||||
img.set({
|
||||
id: this.rasterizedImageId,
|
||||
left: bounds.centerX,
|
||||
top: bounds.centerY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
selectable: true,
|
||||
evented: true,
|
||||
});
|
||||
|
||||
this.rasterizedImage = img;
|
||||
|
||||
// 清理临时画布
|
||||
this._cleanupTempCanvas(tempFabricCanvas);
|
||||
|
||||
console.log("✅ 栅格化图像对象创建完成");
|
||||
resolve(img);
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("生成图像时发生错误:", error);
|
||||
this._cleanupTempCanvas(tempFabricCanvas);
|
||||
reject(error);
|
||||
}
|
||||
}, 100); // 给一点时间确保渲染完成
|
||||
} catch (error) {
|
||||
console.error("创建栅格化图像时发生错误:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算对象边界
|
||||
* @param {Array} objects 对象数组
|
||||
* @returns {Object|null} 边界信息
|
||||
* @private
|
||||
*/
|
||||
_calculateObjectsBounds(objects) {
|
||||
if (!objects || objects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minLeft = Infinity;
|
||||
let minTop = Infinity;
|
||||
let maxRight = -Infinity;
|
||||
let maxBottom = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
try {
|
||||
// 使用更准确的边界计算,不包含绝对变换
|
||||
const bounds = obj.getBoundingRect(false, false);
|
||||
|
||||
console.log(`📐 对象 ${obj.id || obj.type} 边界:`, bounds);
|
||||
|
||||
minLeft = Math.min(minLeft, bounds.left);
|
||||
minTop = Math.min(minTop, bounds.top);
|
||||
maxRight = Math.max(maxRight, bounds.left + bounds.width);
|
||||
maxBottom = Math.max(maxBottom, bounds.top + bounds.height);
|
||||
} catch (error) {
|
||||
console.warn(`计算对象 ${obj.id || obj.type} 边界时发生错误:`, error);
|
||||
// 备选方案:使用对象的基础位置信息
|
||||
const left = obj.left || 0;
|
||||
const top = obj.top || 0;
|
||||
const width = (obj.width || 100) * (obj.scaleX || 1);
|
||||
const height = (obj.height || 100) * (obj.scaleY || 1);
|
||||
|
||||
minLeft = Math.min(minLeft, left - width / 2);
|
||||
minTop = Math.min(minTop, top - height / 2);
|
||||
maxRight = Math.max(maxRight, left + width / 2);
|
||||
maxBottom = Math.max(maxBottom, top + height / 2);
|
||||
}
|
||||
});
|
||||
|
||||
const padding = 10; // 增加边距确保不被裁剪
|
||||
|
||||
const bounds = {
|
||||
left: minLeft - padding,
|
||||
top: minTop - padding,
|
||||
width: maxRight - minLeft + padding * 2,
|
||||
height: maxBottom - minTop + padding * 2,
|
||||
centerX: (minLeft + maxRight) / 2,
|
||||
centerY: (minTop + maxBottom) / 2,
|
||||
};
|
||||
|
||||
// 确保最小尺寸
|
||||
bounds.width = Math.max(bounds.width, 50);
|
||||
bounds.height = Math.max(bounds.height, 50);
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时画布
|
||||
* @param {Object} bounds 边界信息
|
||||
* @returns {HTMLCanvasElement} 临时画布
|
||||
* @private
|
||||
*/
|
||||
_createTempCanvas(bounds) {
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = Math.ceil(bounds.width);
|
||||
tempCanvas.height = Math.ceil(bounds.height);
|
||||
|
||||
const ctx = tempCanvas.getContext("2d");
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
|
||||
// 设置透明背景
|
||||
ctx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
|
||||
console.log(
|
||||
`🖼️ 创建临时画布尺寸: ${tempCanvas.width} x ${tempCanvas.height}`
|
||||
);
|
||||
|
||||
return tempCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加对象到临时画布
|
||||
* @param {fabric.StaticCanvas} tempCanvas 临时画布
|
||||
* @param {Array} objects 对象数组
|
||||
* @param {Object} bounds 边界信息
|
||||
* @private
|
||||
*/
|
||||
_addObjectsToTempCanvas(tempCanvas, objects, bounds) {
|
||||
console.log(`📥 开始添加 ${objects.length} 个对象到临时画布`);
|
||||
|
||||
objects.forEach((obj, index) => {
|
||||
try {
|
||||
// 深度克隆对象,避免影响原对象
|
||||
const clonedObj = fabric.util.object.clone(obj);
|
||||
|
||||
// 计算对象在临时画布中的新位置
|
||||
// 将对象从原始画布坐标系转换到临时画布坐标系
|
||||
const newLeft = obj.left - bounds.left;
|
||||
const newTop = obj.top - bounds.top;
|
||||
|
||||
// 设置对象在临时画布中的位置,保持原有的originX和originY
|
||||
clonedObj.set({
|
||||
left: newLeft,
|
||||
top: newTop,
|
||||
// 保持原有的变换属性
|
||||
scaleX: obj.scaleX,
|
||||
scaleY: obj.scaleY,
|
||||
angle: obj.angle,
|
||||
flipX: obj.flipX,
|
||||
flipY: obj.flipY,
|
||||
opacity: obj.opacity,
|
||||
originX: obj.originX,
|
||||
originY: obj.originY,
|
||||
// 确保可见
|
||||
visible: true,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`📍 对象 ${index + 1} 位置转换: (${obj.left}, ${
|
||||
obj.top
|
||||
}) -> (${newLeft}, ${newTop})`
|
||||
);
|
||||
|
||||
tempCanvas.add(clonedObj);
|
||||
} catch (error) {
|
||||
console.error(`添加对象 ${obj.id || obj.type} 到临时画布失败:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`✅ 临时画布对象添加完成,共 ${tempCanvas.getObjects().length} 个对象`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时画布
|
||||
* @param {fabric.StaticCanvas} tempCanvas 临时画布
|
||||
* @private
|
||||
*/
|
||||
_cleanupTempCanvas(tempCanvas) {
|
||||
try {
|
||||
if (tempCanvas) {
|
||||
tempCanvas.clear();
|
||||
tempCanvas.dispose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("清理临时画布时发生错误:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建栅格化图层并替换原图层
|
||||
* @param {fabric.Image} rasterizedImage 栅格化后的图像
|
||||
* @private
|
||||
*/
|
||||
async _createRasterizedLayer(rasterizedImage) {
|
||||
// 从画布中移除原有对象
|
||||
this.objectsToRasterize.forEach((obj) => {
|
||||
removeCanvasObjectByObject(this.canvas, obj);
|
||||
});
|
||||
|
||||
// 添加栅格化图像到画布
|
||||
this.canvas.add(rasterizedImage);
|
||||
this.canvas.setActiveObject(rasterizedImage);
|
||||
|
||||
// 生成新图层ID
|
||||
this.rasterizedLayerId = generateId("rasterized_layer_");
|
||||
|
||||
// 创建新的栅格化图层
|
||||
this.rasterizedLayer = createLayer({
|
||||
id: this.rasterizedLayerId,
|
||||
name: `${this.layer.name} (栅格化)`,
|
||||
type: LayerType.BITMAP,
|
||||
visible: this.layer.visible,
|
||||
locked: this.layer.locked,
|
||||
opacity: this.layer.opacity,
|
||||
fabricObjects: [rasterizedImage],
|
||||
});
|
||||
|
||||
// 更新图像对象的图层关联
|
||||
rasterizedImage.set({
|
||||
layerId: this.rasterizedLayerId,
|
||||
layerName: this.rasterizedLayer.name,
|
||||
});
|
||||
|
||||
// 替换图层结构
|
||||
if (this.isGroupLayer) {
|
||||
// 组图层:移除所有相关图层
|
||||
const layerIdsToRemove = this.layersToRasterize.map((layer) => layer.id);
|
||||
this.layers.value = this.layers.value.filter(
|
||||
(layer) => !layerIdsToRemove.includes(layer.id)
|
||||
);
|
||||
} else {
|
||||
// 普通图层:移除原图层
|
||||
const layerIndex = this.layers.value.findIndex(
|
||||
(l) => l.id === this.layerId
|
||||
);
|
||||
if (layerIndex !== -1) {
|
||||
this.layers.value.splice(layerIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 在适当位置添加新的栅格化图层
|
||||
const insertIndex = this.layers.value.findIndex(
|
||||
(l) => l.id === (this.parentLayer?.id || this.layerId)
|
||||
);
|
||||
if (insertIndex !== -1) {
|
||||
this.layers.value.splice(insertIndex, 0, this.rasterizedLayer);
|
||||
} else {
|
||||
this.layers.value.push(this.rasterizedLayer);
|
||||
}
|
||||
|
||||
// 设置为活动图层
|
||||
this.activeLayerId.value = this.rasterizedLayerId;
|
||||
|
||||
// 重新渲染画布
|
||||
await optimizeCanvasRendering(this.canvas, () => {
|
||||
this.canvas.renderAll();
|
||||
});
|
||||
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
|
||||
console.log(`🎨 栅格化图层 ${this.rasterizedLayer.name} 创建完成`);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,18 @@ export class RemoveLayerCommand extends Command {
|
||||
);
|
||||
this.removedLayer = this.layers.value[this.layerIndex];
|
||||
this.isActiveLayer = this.layerId === this.activeLayerId.value;
|
||||
// this.beforeLayers = [...this.layers.value]; // 备份原图层列表
|
||||
|
||||
// 从Canvas中找到真实对象并备份,确保撤销和重做时对象的一致性
|
||||
this.originalObjects = [];
|
||||
if (this.removedLayer) {
|
||||
// 从画布中获取真实的对象引用
|
||||
this.originalObjects = this.canvas.getObjects().filter((obj) => {
|
||||
return obj.layerId === this.layerId;
|
||||
});
|
||||
}
|
||||
|
||||
// 备份原活动图层ID
|
||||
this.originalActiveLayerId = this.activeLayerId.value;
|
||||
}
|
||||
|
||||
execute() {
|
||||
@@ -311,19 +322,22 @@ export class RemoveLayerCommand extends Command {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从画布中移除图层中的所有对象
|
||||
if (
|
||||
this.removedLayer.fabricObjects &&
|
||||
this.removedLayer.fabricObjects.length > 0
|
||||
) {
|
||||
this.removedLayer.fabricObjects.forEach((obj) => {
|
||||
// 从画布中移除图层中的所有真实对象
|
||||
this.originalObjects.forEach((obj) => {
|
||||
if (this.canvas.getObjects().includes(obj)) {
|
||||
this.canvas.remove(obj);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果是背景图层,移除特殊对象
|
||||
if (this.removedLayer.isBackground && this.removedLayer.fabricObject) {
|
||||
this.canvas.remove(this.removedLayer.fabricObject);
|
||||
const { object } = findObjectById(
|
||||
this.canvas,
|
||||
this.removedLayer.fabricObject.id
|
||||
);
|
||||
if (object) {
|
||||
this.canvas.remove(object);
|
||||
}
|
||||
}
|
||||
|
||||
// 从图层列表中删除
|
||||
@@ -347,38 +361,49 @@ export class RemoveLayerCommand extends Command {
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 已移除图层: ${this.removedLayer.name} (ID: ${this.layerId})`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
undo() {
|
||||
// 恢复图层
|
||||
// 恢复图层到原位置
|
||||
if (this.layerIndex !== -1 && this.removedLayer) {
|
||||
this.layers.value.splice(this.layerIndex, 0, this.removedLayer);
|
||||
|
||||
// 恢复图层中的所有对象到画布
|
||||
if (
|
||||
this.removedLayer.fabricObjects &&
|
||||
this.removedLayer.fabricObjects.length > 0
|
||||
) {
|
||||
this.removedLayer.fabricObjects.forEach((obj) => {
|
||||
// 使用优化渲染批处理恢复真实对象到画布
|
||||
optimizeCanvasRendering(this.canvas, () => {
|
||||
this.originalObjects.forEach((obj) => {
|
||||
// 恢复对象到画布
|
||||
this.canvas.add(obj);
|
||||
// 确保对象的图层信息正确
|
||||
obj.layerId = this.layerId;
|
||||
obj.layerName = this.removedLayer.name;
|
||||
obj.setCoords(); // 更新坐标
|
||||
});
|
||||
}
|
||||
|
||||
// 如果是背景图层,恢复特殊对象
|
||||
if (this.removedLayer.isBackground && this.removedLayer.fabricObject) {
|
||||
this.canvas.add(this.removedLayer.fabricObject);
|
||||
}
|
||||
// 如果是背景图层,恢复特殊对象
|
||||
if (this.removedLayer.isBackground && this.removedLayer.fabricObject) {
|
||||
// 检查对象是否已在画布中
|
||||
const { object } = findObjectById(
|
||||
this.canvas,
|
||||
this.removedLayer.fabricObject.id
|
||||
);
|
||||
if (!object) {
|
||||
this.canvas.add(this.removedLayer.fabricObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果删除的是当前活动图层,恢复活动图层
|
||||
if (this.isActiveLayer) {
|
||||
this.activeLayerId.value = this.layerId;
|
||||
}
|
||||
|
||||
// 重新渲染画布
|
||||
if (this.canvas) {
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
console.log(
|
||||
`↩️ 已恢复图层: ${this.removedLayer.name} (ID: ${this.layerId})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +412,7 @@ export class RemoveLayerCommand extends Command {
|
||||
name: this.name,
|
||||
layerName: this.removedLayer?.name || "未知图层",
|
||||
layerId: this.layerId,
|
||||
objectCount: this.originalObjects.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -503,56 +529,40 @@ export class ToggleLayerVisibilityCommand extends Command {
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.layerId = options.layerId;
|
||||
this.layerManager = options.layerManager;
|
||||
|
||||
// 查找图层
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
this.layer = layer;
|
||||
this.oldVisibility = this.layer ? this.layer.visible : null;
|
||||
// this.oldVisibility = this.layer ? this.layer.visible : null;
|
||||
}
|
||||
|
||||
execute() {
|
||||
async execute() {
|
||||
if (!this.layer) {
|
||||
console.error(`图层 ${this.layerId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 切换可见性
|
||||
this.layer.visible = !this.oldVisibility;
|
||||
this.layer.visible = !this.layer.visible;
|
||||
|
||||
// 更新画布上图层对象的可见性
|
||||
if (this.canvas) {
|
||||
const layerObjects = this.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.layerId === this.layerId);
|
||||
|
||||
layerObjects.forEach((obj) => {
|
||||
obj.visible = this.layer.visible;
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
// 更新画布上对象的可选择状态
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
|
||||
return true;
|
||||
return this.layer.visible;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.layer) {
|
||||
// 恢复可见性
|
||||
this.layer.visible = this.oldVisibility;
|
||||
|
||||
// 更新画布上图层对象的可见性
|
||||
if (this.canvas) {
|
||||
const layerObjects = this.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.layerId === this.layerId);
|
||||
|
||||
layerObjects.forEach((obj) => {
|
||||
obj.visible = this.oldVisibility;
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
}
|
||||
async undo() {
|
||||
return await this.execute(); // 直接调用execute方法来恢复可见性
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
@@ -565,6 +575,71 @@ export class ToggleLayerVisibilityCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换子图层可见性命令
|
||||
*/
|
||||
export class ToggleChildLayerVisibilityCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "切换子图层可见性",
|
||||
saveState: false,
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.layerId = options.layerId;
|
||||
this.parentId = options.parentId;
|
||||
this.layerManager = options.layerManager;
|
||||
|
||||
// 查找父图层和子图层
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
this.layerId
|
||||
);
|
||||
this.parentLayer = parent;
|
||||
this.childLayer = layer;
|
||||
|
||||
// this.oldVisibility = this.childLayer ? this.childLayer.visible : null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (!this.childLayer) {
|
||||
throw new Error("找不到要切换可见性的子图层");
|
||||
}
|
||||
|
||||
// 切换可见性
|
||||
this.childLayer.visible = !this.childLayer.visible;
|
||||
|
||||
// 更新画布上图层对象的可见性
|
||||
if (this.canvas) {
|
||||
const layerObjects = this.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.layerId === this.layerId);
|
||||
|
||||
layerObjects.forEach((obj) => {
|
||||
obj.visible = this.childLayer.visible;
|
||||
});
|
||||
}
|
||||
|
||||
// 更新画布上对象的可选择状态
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
return this.childLayer.visible;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
return await this.execute(); // 直接调用execute方法来恢复可见性
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
layerName: this.childLayer?.name || "未知子图层",
|
||||
layerId: this.layerId,
|
||||
parentId: this.parentId,
|
||||
newVisibility: this.childLayer?.visible,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名图层命令
|
||||
*/
|
||||
@@ -1031,9 +1106,9 @@ export class GroupLayersCommand extends Command {
|
||||
// 备份原图层
|
||||
this.originalLayers = [...this.layers.value];
|
||||
// 新组ID
|
||||
this.groupId = `group_layer_${Date.now()}_${Math.floor(
|
||||
Math.random() * 1000
|
||||
)}`;
|
||||
this.groupId =
|
||||
generateId("group_layer_") ||
|
||||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
this.originalActiveLayerId = this.activeLayerId.value; // 备份原活动图层ID
|
||||
}
|
||||
@@ -1097,6 +1172,8 @@ export class GroupLayersCommand extends Command {
|
||||
// 更新当前活动图层
|
||||
this.activeLayerId.value = this.layerIds[0];
|
||||
|
||||
this.canvas?.thumbnailManager?.generateLayerThumbnail?.(this.groupId);
|
||||
|
||||
return this.groupId;
|
||||
}
|
||||
|
||||
@@ -2199,9 +2276,9 @@ export class LayerObjectsToGroupCommand extends Command {
|
||||
* @private
|
||||
*/
|
||||
_updateThumbnail() {
|
||||
if (this.canvas.thumbnailManager) {
|
||||
this.canvas.thumbnailManager.generateLayerThumbnail(this.activeLayer.id);
|
||||
}
|
||||
// this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
|
||||
// this.activeLayer.id
|
||||
// );
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
@@ -2232,8 +2309,12 @@ export class CreateImageLayerCommand extends Command {
|
||||
this.toolManager = options.toolManager;
|
||||
this.layerName = options.layerName || null;
|
||||
|
||||
this.imageId = generateId("image_");
|
||||
|
||||
// 存储执行过程中的结果
|
||||
this.newLayerId = null;
|
||||
this.newLayerId =
|
||||
generateId("layer_image_") ||
|
||||
`layer_image_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
this.commands = [];
|
||||
this.executedCommands = [];
|
||||
}
|
||||
@@ -2251,12 +2332,15 @@ export class CreateImageLayerCommand extends Command {
|
||||
const fileName =
|
||||
this.layerName || `图片 ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
this.fabricImage.set({
|
||||
id: this.imageId,
|
||||
});
|
||||
// 1. 创建新图层命令
|
||||
const createLayerCmd = new AddLayerCommand({
|
||||
canvas: this.layerManager.canvas,
|
||||
layers: this.layerManager.layers,
|
||||
newLayer: createLayer({
|
||||
id: this.newLayerId || generateId("layer"),
|
||||
id: this.newLayerId,
|
||||
name: fileName,
|
||||
type: LayerType.BITMAP,
|
||||
visible: true,
|
||||
@@ -2658,6 +2742,11 @@ export class CutLayerCommand extends Command {
|
||||
this.cutLayer = null;
|
||||
this.clipboardData = null;
|
||||
this.wasActiveLayer = false;
|
||||
|
||||
// 生成新图层ID和名称
|
||||
this.newLayerId =
|
||||
generateId("layer_") ||
|
||||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
@@ -2678,9 +2767,6 @@ export class CutLayerCommand extends Command {
|
||||
console.warn("不能复制背景图层");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成新图层ID和名称
|
||||
this.newLayerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
const newName = `${sourceLayer.name} copy`;
|
||||
|
||||
// 创建新图层
|
||||
@@ -2781,100 +2867,6 @@ export class CutLayerCommand extends Command {
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
sourceLayerId: this.layerId,
|
||||
newLayerId: this.newLayerId,
|
||||
newLayerName: this.newLayer?.name,
|
||||
objectCount: this.createdObjects.length,
|
||||
isChildLayer: this.isChildLayer,
|
||||
parentLayerId: this.parentLayer?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制图层到指定位置命令
|
||||
*/
|
||||
export class DuplicateLayerCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "复制图层",
|
||||
saveState: true,
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.layerId = options.layerId;
|
||||
this.activeLayerId = options.activeLayerId;
|
||||
this.insertIndex = options.insertIndex || null;
|
||||
this.layerManager = options.layerManager;
|
||||
|
||||
this.newLayer = null;
|
||||
this.newLayerId = null;
|
||||
this.createdObjects = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
const sourceLayer = layer;
|
||||
if (!sourceLayer) {
|
||||
console.error(`源图层 ${this.layerId} 不存在`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 不允许复制背景图层
|
||||
if (sourceLayer.isBackground) {
|
||||
console.warn("不能复制背景图层");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成新图层ID和名称
|
||||
this.newLayerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
const newName = `${sourceLayer.name} copy`;
|
||||
|
||||
// 创建新图层
|
||||
this.newLayer = createLayer({
|
||||
id: this.newLayerId,
|
||||
name: newName,
|
||||
type: sourceLayer.type,
|
||||
visible: sourceLayer.visible,
|
||||
locked: sourceLayer.locked,
|
||||
opacity: sourceLayer.opacity,
|
||||
blendMode: sourceLayer.blendMode,
|
||||
fabricObjects: [],
|
||||
children: sourceLayer.children ? [...sourceLayer.children] : [],
|
||||
layerProperties: sourceLayer.layerProperties
|
||||
? { ...sourceLayer.layerProperties }
|
||||
: {},
|
||||
metadata: sourceLayer.metadata ? { ...sourceLayer.metadata } : {},
|
||||
});
|
||||
|
||||
// 计算插入位置
|
||||
const sourceIndex = this.layers.value.findIndex(
|
||||
(l) => l.id === this.layerId
|
||||
);
|
||||
const insertIndex =
|
||||
this.insertIndex !== null ? this.insertIndex : sourceIndex + 1;
|
||||
|
||||
// 插入新图层
|
||||
this.layers.value.splice(insertIndex, 0, this.newLayer);
|
||||
|
||||
// 复制源图层中的对象
|
||||
if (sourceLayer.fabricObjects && sourceLayer.fabricObjects.length > 0) {
|
||||
await this._duplicateObjects(sourceLayer.fabricObjects);
|
||||
}
|
||||
|
||||
// 设置为活动图层
|
||||
this.activeLayerId.value = this.newLayerId;
|
||||
|
||||
// 重新渲染画布
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
|
||||
console.log(`已复制图层:${newName}`);
|
||||
return this.newLayerId;
|
||||
}
|
||||
|
||||
async _duplicateObjects(sourceObjects) {
|
||||
const serializedObjects = sourceObjects.map((obj) => {
|
||||
// 序列化对象时保留必要的属性
|
||||
@@ -2914,27 +2906,6 @@ export class DuplicateLayerCommand extends Command {
|
||||
});
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (!this.newLayer) return;
|
||||
|
||||
// 从图层列表中删除新图层
|
||||
const index = this.layers.value.findIndex((l) => l.id === this.newLayerId);
|
||||
if (index !== -1) {
|
||||
this.layers.value.splice(index, 1);
|
||||
}
|
||||
|
||||
// 从画布中移除所有创建的对象
|
||||
this.createdObjects.forEach((obj) => {
|
||||
this.canvas.remove(obj);
|
||||
});
|
||||
|
||||
// 恢复原活动图层
|
||||
this.activeLayerId.value = this.layerId;
|
||||
|
||||
// 重新渲染画布
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -2942,6 +2913,8 @@ export class DuplicateLayerCommand extends Command {
|
||||
newLayerId: this.newLayerId,
|
||||
newLayerName: this.newLayer?.name,
|
||||
objectCount: this.createdObjects.length,
|
||||
isChildLayer: this.isChildLayer,
|
||||
parentLayerId: this.parentLayer?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2963,15 +2936,13 @@ export class CreateAdjustmentLayerCommand extends Command {
|
||||
this.insertIndex = options.insertIndex || null;
|
||||
|
||||
this.newLayer = null;
|
||||
this.newLayerId = null;
|
||||
// 生成新图层ID
|
||||
this.newLayerId =
|
||||
generateId("adj_layer_") ||
|
||||
`adj_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
execute() {
|
||||
// 生成新图层ID
|
||||
this.newLayerId = `adj_layer_${Date.now()}_${Math.floor(
|
||||
Math.random() * 1000
|
||||
)}`;
|
||||
|
||||
// 创建调整图层
|
||||
this.newLayer = createLayer({
|
||||
id: this.newLayerId,
|
||||
@@ -3959,6 +3930,10 @@ export class RemoveChildLayerCommand extends Command {
|
||||
) ?? -1;
|
||||
this.removedChild = this.parentLayer?.children?.[this.childIndex];
|
||||
this.isActiveLayer = this.layerId === this.activeLayerId.value;
|
||||
|
||||
this.originalObjects = this.canvas.getObjects().filter((obj) => {
|
||||
return obj.layerId === this.layerId;
|
||||
});
|
||||
}
|
||||
|
||||
async execute() {
|
||||
@@ -3972,8 +3947,9 @@ export class RemoveChildLayerCommand extends Command {
|
||||
this.removedChild.fabricObjects.length > 0
|
||||
) {
|
||||
this.removedChild.fabricObjects.forEach((obj) => {
|
||||
if (this.canvas.getObjects().includes(obj)) {
|
||||
this.canvas.remove(obj);
|
||||
const { object } = findObjectById(this.canvas, obj.id);
|
||||
if (object) {
|
||||
this.canvas.remove(...this.originalObjects);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4001,29 +3977,34 @@ export class RemoveChildLayerCommand extends Command {
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (!this.parentLayer || this.childIndex === -1 || !this.removedChild)
|
||||
if (
|
||||
!this.parentLayer ||
|
||||
this.childIndex === -1 ||
|
||||
!this.removedChild ||
|
||||
this.originalObjects.length === 0
|
||||
) {
|
||||
return;
|
||||
|
||||
}
|
||||
// 恢复子图层到原位置
|
||||
this.parentLayer.children.splice(this.childIndex, 0, this.removedChild);
|
||||
|
||||
// 恢复子图层中的所有对象到画布
|
||||
if (
|
||||
this.removedChild.fabricObjects &&
|
||||
this.removedChild.fabricObjects.length > 0
|
||||
) {
|
||||
this.removedChild.fabricObjects.forEach((obj) => {
|
||||
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();
|
||||
// 重新渲染画布
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
});
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
@@ -4117,6 +4098,7 @@ export class ChildLayerLockCommand extends Command {
|
||||
this.layers = options.layers;
|
||||
this.layerId = options.layerId;
|
||||
this.parentId = options.parentId;
|
||||
this.layerManager = options.layerManager;
|
||||
|
||||
// 查找父图层和子图层
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
@@ -4163,61 +4145,3 @@ export class ChildLayerLockCommand extends Command {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换子图层可见性命令
|
||||
*/
|
||||
export class ToggleChildLayerVisibilityCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "切换子图层可见性",
|
||||
saveState: false,
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.layerId = options.layerId;
|
||||
this.parentId = options.parentId;
|
||||
|
||||
// 查找父图层和子图层
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
this.layerId
|
||||
);
|
||||
this.parentLayer = parent;
|
||||
this.childLayer = layer;
|
||||
|
||||
this.oldVisibility = this.childLayer ? this.childLayer.visible : null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (!this.childLayer) {
|
||||
throw new Error("找不到要切换可见性的子图层");
|
||||
}
|
||||
|
||||
// 切换可见性
|
||||
this.childLayer.visible = !this.oldVisibility;
|
||||
|
||||
// 更新画布上对象的可选择状态
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (this.childLayer) {
|
||||
this.childLayer.visible = this.oldVisibility;
|
||||
|
||||
// 更新画布上对象的可选择状态
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
}
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
layerName: this.childLayer?.name || "未知子图层",
|
||||
layerId: this.layerId,
|
||||
parentId: this.parentId,
|
||||
newVisibility: this.childLayer?.visible,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ export class AddObjectToLayerCommand extends Command {
|
||||
this.canvas.remove(object);
|
||||
}
|
||||
|
||||
this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
// 重置为首次执行状态,以便重做时能正确恢复位置
|
||||
this.isFirstExecution = false; // 保持为false,因为已经执行过一次了
|
||||
});
|
||||
@@ -473,18 +473,28 @@ export class ChangeFixedImageCommand extends Command {
|
||||
this.scale = options.scale || { x: 1, y: 1 };
|
||||
this.preserveTransform = options.preserveTransform ?? false; // 默认不保留变换
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.scale.x = options.scaleX || 1;
|
||||
this.scale.y = options.scaleY || 1;
|
||||
this.position.x = options.left || this.canvas.width / 2;
|
||||
this.position.y = options.top || this.canvas.height / 2;
|
||||
|
||||
// 用于回滚的状态
|
||||
this.previousImage = null;
|
||||
this.previousTransform = null;
|
||||
this.previousObjectId = null; // 保存之前对象的ID
|
||||
this.targetLayer = null;
|
||||
this.newObjectId = null; // 保存新对象的ID
|
||||
this.newObjectId = generateId("fixed_"); // 保存新对象的ID
|
||||
this.isExecuted = false;
|
||||
|
||||
// 错误处理
|
||||
this.maxRetries = options.maxRetries || 3;
|
||||
this.retryCount = 0;
|
||||
this.timeoutMs = options.timeoutMs || 10000;
|
||||
|
||||
// 查找目标图层
|
||||
this.targetLayer = this.findTargetLayer();
|
||||
}
|
||||
|
||||
async execute() {
|
||||
@@ -668,12 +678,9 @@ export class ChangeFixedImageCommand extends Command {
|
||||
|
||||
async applyImageToLayer(newImage) {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 生成新对象ID
|
||||
this.newObjectId = generateId();
|
||||
|
||||
// 设置基本属性
|
||||
newImage.set({
|
||||
id: this.newObjectId,
|
||||
id: this.targetLayer?.fabricObject?.id || this.newObjectId,
|
||||
layerId: this.targetLayer.id,
|
||||
layerName: this.targetLayer.name,
|
||||
isBackground: this.targetLayer.isBackground,
|
||||
@@ -884,6 +891,8 @@ export class AddImageToLayerCommand extends Command {
|
||||
this.targetLayer = null;
|
||||
this.isExecuted = false;
|
||||
|
||||
this.imageId = generateId("image_");
|
||||
|
||||
// 错误处理
|
||||
this.maxRetries = options.maxRetries || 3;
|
||||
this.retryCount = 0;
|
||||
@@ -907,7 +916,7 @@ export class AddImageToLayerCommand extends Command {
|
||||
const newImage = await this.loadImageWithRetry();
|
||||
|
||||
// 添加图像到图层
|
||||
await this.addImageToLayer(newImage);
|
||||
await this.addImageToLayer(newImage, this.imageId);
|
||||
|
||||
this.isExecuted = true;
|
||||
|
||||
@@ -1061,10 +1070,7 @@ export class AddImageToLayerCommand extends Command {
|
||||
});
|
||||
}
|
||||
|
||||
async addImageToLayer(newImage) {
|
||||
// 生成唯一ID
|
||||
const objectId = generateId();
|
||||
|
||||
async addImageToLayer(newImage, objectId) {
|
||||
// 设置图像属性
|
||||
newImage.set({
|
||||
id: objectId,
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
import { Command } from "./Command";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import {
|
||||
LayerType,
|
||||
OperationType,
|
||||
createLayer,
|
||||
findLayerRecursively,
|
||||
} from "../utils/layerHelper";
|
||||
import {
|
||||
generateId,
|
||||
optimizeCanvasRendering,
|
||||
findObjectById,
|
||||
removeCanvasObjectByObject,
|
||||
} from "../utils/helper";
|
||||
import { createRasterizedImage } from "../utils/rasterizedImage";
|
||||
|
||||
/**
|
||||
* 栅格化图层命令
|
||||
* 将图层中的所有矢量对象转换为位图图像
|
||||
* 支持普通图层和组图层的栅格化
|
||||
*/
|
||||
export class RasterizeLayerCommand 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} 不存在`);
|
||||
}
|
||||
|
||||
// 检查是否可以栅格化
|
||||
if (this.layer.isBackground || this.layer.isFixed) {
|
||||
throw new Error("背景图层和固定图层不能栅格化");
|
||||
}
|
||||
|
||||
try {
|
||||
// 收集要栅格化的图层和对象
|
||||
this._collectLayersAndObjects();
|
||||
|
||||
if (this.objectsToRasterize.length === 0) {
|
||||
throw new Error("图层没有内容可栅格化");
|
||||
}
|
||||
|
||||
// 保存原始对象状态
|
||||
this._saveOriginalObjectStates();
|
||||
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.renderAll();
|
||||
|
||||
// 创建栅格化图像
|
||||
const rasterizedImage = await createRasterizedImage({
|
||||
canvas: this.canvas,
|
||||
fabricObjects: this.objectsToRasterize,
|
||||
});
|
||||
|
||||
// 创建新的栅格化图层并替换原图层
|
||||
await this._createRasterizedLayer(rasterizedImage);
|
||||
|
||||
// 切换到选择工具
|
||||
this.layerManager?.toolManager?.setTool?.(OperationType.SELECT);
|
||||
|
||||
console.log(`✅ 图层 ${this.layer.name} 栅格化完成`);
|
||||
|
||||
this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
|
||||
this.rasterizedLayerId
|
||||
);
|
||||
return this.rasterizedLayerId;
|
||||
} catch (error) {
|
||||
console.error("栅格化图层失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (!this.originalLayers || !this.originalCanvasObjects) {
|
||||
throw new Error("没有可恢复的原始数据");
|
||||
}
|
||||
|
||||
try {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 清空画布
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.clear();
|
||||
|
||||
// 恢复原始对象及其状态
|
||||
this.originalCanvasObjects.forEach((obj) => {
|
||||
// 如果保存了该对象的原始状态,则恢复状态
|
||||
if (this.originalObjectStates.has(obj.id)) {
|
||||
const originalState = this.originalObjectStates.get(obj.id);
|
||||
obj.set(originalState);
|
||||
}
|
||||
|
||||
this.canvas.add(obj);
|
||||
obj.setCoords();
|
||||
});
|
||||
|
||||
// 恢复原始图层结构
|
||||
this.layers.value = [...this.originalLayers];
|
||||
|
||||
// 恢复原活动图层
|
||||
this.activeLayerId.value = this.layerId;
|
||||
|
||||
// 更新画布交互性
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
});
|
||||
|
||||
console.log(`↩️ 图层 ${this.layer.name} 栅格化已撤销`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("撤销栅格化失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集要栅格化的图层和对象
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存原始对象状态
|
||||
* @private
|
||||
*/
|
||||
_saveOriginalObjectStates() {
|
||||
this.objectsToRasterize.forEach((object) => {
|
||||
if (object && object.id) {
|
||||
const originalState = {
|
||||
left: object.left,
|
||||
top: object.top,
|
||||
scaleX: object.scaleX,
|
||||
scaleY: object.scaleY,
|
||||
angle: object.angle,
|
||||
flipX: object.flipX,
|
||||
flipY: object.flipY,
|
||||
opacity: object.opacity,
|
||||
originX: object.originX,
|
||||
originY: object.originY,
|
||||
layerId: object.layerId,
|
||||
layerName: object.layerName,
|
||||
width: object.width,
|
||||
height: object.height,
|
||||
strokeWidth: object.strokeWidth,
|
||||
visible: object.visible,
|
||||
};
|
||||
this.originalObjectStates.set(object.id, originalState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建栅格化图层并替换原图层
|
||||
* @param {fabric.Image} rasterizedImage 栅格化后的图像
|
||||
* @private
|
||||
*/
|
||||
async _createRasterizedLayer(rasterizedImage) {
|
||||
// 从画布中移除原有对象
|
||||
this.objectsToRasterize.forEach((obj) => {
|
||||
removeCanvasObjectByObject(this.canvas, obj);
|
||||
});
|
||||
|
||||
// 添加栅格化图像到画布
|
||||
this.canvas.add(rasterizedImage);
|
||||
this.canvas.setActiveObject(rasterizedImage);
|
||||
|
||||
// 创建新的栅格化图层
|
||||
this.rasterizedLayer = createLayer({
|
||||
id: this.rasterizedLayerId,
|
||||
name: `${this.layer.name} (栅格化)`,
|
||||
type: LayerType.BITMAP,
|
||||
visible: this.layer.visible,
|
||||
locked: this.layer.locked,
|
||||
opacity: this.layer.opacity,
|
||||
fabricObjects: [rasterizedImage],
|
||||
});
|
||||
|
||||
// 更新图像对象的图层关联
|
||||
rasterizedImage.set({
|
||||
id: this.resterizedId,
|
||||
type: "image",
|
||||
layerId: this.rasterizedLayerId,
|
||||
layerName: this.rasterizedLayer.name,
|
||||
});
|
||||
|
||||
// 在适当位置添加新的栅格化图层
|
||||
// 1.当前如果是子图层,则插入到子图层的位置
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
this.layerId
|
||||
);
|
||||
|
||||
let insertIndex = 0;
|
||||
// 说明是子图层
|
||||
if (parent) {
|
||||
this.layers.value.some((l, index) => {
|
||||
if (l.id === parent.id) {
|
||||
insertIndex = this.layers.value?.[index].children?.findIndex(
|
||||
(fItem) => fItem.id === this.layerId
|
||||
);
|
||||
return true; // 找到父图层,停止循环
|
||||
}
|
||||
return false; // 继续查找
|
||||
});
|
||||
} else {
|
||||
insertIndex = this.layers.value.findIndex((l) => l.id === this.layerId);
|
||||
}
|
||||
|
||||
if (insertIndex !== -1) {
|
||||
if (parent) {
|
||||
const pIndex = this.layers.value.findIndex((l) => l.id === parent.id);
|
||||
this.layers.value[pIndex].children?.splice?.(
|
||||
insertIndex,
|
||||
1,
|
||||
this.rasterizedLayer
|
||||
);
|
||||
} else this.layers.value.splice(insertIndex, 1, this.rasterizedLayer);
|
||||
} else {
|
||||
// 2.如果没有找到父图层,则添加到顶层
|
||||
this.layers.value.unshift(this.rasterizedLayer);
|
||||
}
|
||||
|
||||
// // 替换图层结构
|
||||
// if (this.isGroupLayer) {
|
||||
// // 组图层:移除所有相关图层
|
||||
// const layerIdsToRemove = this.layersToRasterize.map((layer) => layer.id);
|
||||
// this.layers.value = this.layers.value.filter(
|
||||
// (layer) => !layerIdsToRemove.includes(layer.id)
|
||||
// );
|
||||
// } else {
|
||||
// // 普通图层:移除原图层
|
||||
// const layerIndex = this.layers.value.findIndex(
|
||||
// (l) => l.id === this.layerId
|
||||
// );
|
||||
// if (layerIndex !== -1) {
|
||||
// this.layers.value.splice(layerIndex, 1);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 设置为活动图层
|
||||
this.activeLayerId.value = this.rasterizedLayerId;
|
||||
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
|
||||
console.log(`🎨 栅格化图层 ${this.rasterizedLayer.name} 创建完成`);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
// 存储加载的图片对象
|
||||
this.clothingImage = null;
|
||||
this.redGreenImage = null;
|
||||
this.redGreenImageMask = null;
|
||||
|
||||
// 存储新创建的图层ID
|
||||
this.newEmptyLayerId = null;
|
||||
@@ -68,7 +69,11 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
// 3. 保存原始状态
|
||||
this.originalBackgroundObject = backgroundLayer.fabricObject
|
||||
? {
|
||||
...backgroundLayer.fabricObject.toObject(),
|
||||
...backgroundLayer.fabricObject.toObject([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
]),
|
||||
ref: backgroundLayer.fabricObject,
|
||||
}
|
||||
: null;
|
||||
@@ -136,12 +141,31 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
// clipPathImg.set({
|
||||
// absolutePositioned: true,
|
||||
// });
|
||||
this.redGreenImage.set({
|
||||
absolutePositioned: true,
|
||||
|
||||
// 克隆衣服底图作为裁剪对象
|
||||
this.redGreenImageMask = await new Promise((resolve, reject) => {
|
||||
this.redGreenImage.clone((clonedImg) => {
|
||||
if (!clonedImg) {
|
||||
reject(new Error("无法克隆红绿图"));
|
||||
return;
|
||||
}
|
||||
resolve(clonedImg);
|
||||
});
|
||||
});
|
||||
|
||||
this.redGreenImageMask.set({
|
||||
absolutePositioned: true,
|
||||
opacity: 0.01, // 设置为几乎透明
|
||||
type: "redGreenImageMask",
|
||||
id: generateId("redGreenImageMask_"),
|
||||
});
|
||||
// this.canvas.add(this.redGreenImageMask);
|
||||
this.canvas.clipPath = this.redGreenImageMask;
|
||||
this.redGreenImageMask.sendToBack();
|
||||
this.redGreenImageMask.setCoords();
|
||||
|
||||
const activeLayer = this.layerManager.getActiveLayer();
|
||||
activeLayer.clippingMask = this.redGreenImage.toObject(["id"]);
|
||||
// activeLayer.clippingMask = this.redGreenImageMask.toObject(["id"]);
|
||||
activeLayer.opacity = this.normalLayerOpacity;
|
||||
// activeLayer?.fabricObjects.forEach((obj) => {
|
||||
// obj.set({
|
||||
@@ -181,9 +205,9 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
async _createAndActivateEmptyLayer() {
|
||||
// 创建新的空白图层
|
||||
const newLayerName = "绘制图层";
|
||||
const newLayerId = this.layerManager.createLayer(
|
||||
const newLayerId = await this.layerManager.createLayer(
|
||||
newLayerName,
|
||||
LayerType.GROUP,
|
||||
LayerType.BITMAP,
|
||||
{
|
||||
undoable: false,
|
||||
}
|
||||
@@ -306,7 +330,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
*/
|
||||
async _setupBackgroundLayer(backgroundLayer, clothingImage) {
|
||||
let backgroundObject = backgroundLayer.fabricObject;
|
||||
let { object } = findObjectById(this.canvas, backgroundObject.id);
|
||||
const { object } = findObjectById(this.canvas, backgroundObject.id);
|
||||
|
||||
if (!object) {
|
||||
// 创建白色背景矩形
|
||||
@@ -389,12 +413,16 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
|
||||
// 清除固定图层原有内容
|
||||
if (fixedLayer.fabricObject) {
|
||||
this.canvas.remove(fixedLayer.fabricObject);
|
||||
const { object } = findObjectById(
|
||||
this.canvas,
|
||||
fixedLayer.fabricObject.id
|
||||
);
|
||||
if (object) this.canvas.remove(object);
|
||||
}
|
||||
|
||||
// 添加到画布和固定图层
|
||||
this.canvas.add(img);
|
||||
fixedLayer.fabricObject = img;
|
||||
fixedLayer.fabricObject = img.toObject(["id", "type", "layerId"]);
|
||||
this.clothingImage = img;
|
||||
}
|
||||
|
||||
@@ -424,15 +452,15 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
// 清除普通图层原有内容
|
||||
if (normalLayer.fabricObjects) {
|
||||
normalLayer.fabricObjects.forEach((obj) => {
|
||||
this.canvas.remove(obj);
|
||||
const { object } = findObjectById(this.canvas, obj.id);
|
||||
if (object) {
|
||||
this.canvas.remove(object);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 给img设置裁剪,裁剪图为衣服底图
|
||||
|
||||
// 添加到画布和普通图层
|
||||
this.canvas.add(img);
|
||||
normalLayer.fabricObjects = [img];
|
||||
normalLayer.fabricObjects = [img.toObject(["id", "type", "layerId"])];
|
||||
this.redGreenImage = img;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { generateId, optimizeCanvasRendering } from "../utils/helper";
|
||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper";
|
||||
import { Command } from "./Command";
|
||||
|
||||
/**
|
||||
@@ -302,3 +304,323 @@ export class CompositeTextCommand extends Command {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本命令
|
||||
* 用于创建文本对象和图层的组合操作
|
||||
*/
|
||||
export class CreateTextCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "创建文本",
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.x = options.x;
|
||||
this.y = options.y;
|
||||
this.textOptions = options.textOptions || {};
|
||||
// 生成唯一ID
|
||||
this.textId = options?.textId || this.generateId("text_");
|
||||
this.layerId = options?.layerId || generateId("text_layer_");
|
||||
|
||||
// 生成的对象和图层信息
|
||||
this.textObject = null;
|
||||
this.oldActiveLayerId = null;
|
||||
|
||||
// 默认文本属性
|
||||
this.defaultOptions = {
|
||||
text: "双击编辑文本",
|
||||
fontFamily: "Arial",
|
||||
fontSize: 24,
|
||||
fontWeight: "normal",
|
||||
fontStyle: "normal",
|
||||
textAlign: "left",
|
||||
fill: "#000000",
|
||||
opacity: 1,
|
||||
underline: false,
|
||||
overline: false,
|
||||
linethrough: false,
|
||||
textBackgroundColor: "transparent",
|
||||
lineHeight: 1.16,
|
||||
charSpacing: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (!this.canvas || !this.layerManager) {
|
||||
console.error("Canvas或LayerManager不存在");
|
||||
return null;
|
||||
}
|
||||
// 保存当前活动图层
|
||||
this.oldActiveLayerId = this.layerManager.activeLayerId?.value;
|
||||
|
||||
// 合并默认选项和用户选项
|
||||
const finalOptions = {
|
||||
...this.defaultOptions,
|
||||
...this.textOptions,
|
||||
left: this.x,
|
||||
top: this.y,
|
||||
};
|
||||
|
||||
try {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 创建文本对象
|
||||
this.textObject = new fabric.IText(finalOptions.text, {
|
||||
...finalOptions,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
// 创建文本图层
|
||||
const layerName = this.textOptions.layerName || "文本图层";
|
||||
const layer = createLayer({
|
||||
id: this.layerId,
|
||||
name: layerName,
|
||||
type: LayerType.TEXT,
|
||||
});
|
||||
|
||||
// 设置对象的图层关联
|
||||
this.textObject.set({
|
||||
id: this.textId,
|
||||
layerId: this.layerId,
|
||||
layerName: layerName,
|
||||
});
|
||||
|
||||
// 智能插入图层到合适位置
|
||||
this._insertLayerAtCorrectPosition(layer);
|
||||
|
||||
// 添加到画布
|
||||
this.canvas.add(this.textObject);
|
||||
|
||||
// 取消其他对象的选中状态
|
||||
this.canvas.discardActiveObject();
|
||||
|
||||
// 设置新创建的文本对象为活动对象
|
||||
this.canvas.setActiveObject(this.textObject);
|
||||
|
||||
// 更新图层的对象列表
|
||||
if (layer) {
|
||||
layer.fabricObjects = layer.fabricObjects || [];
|
||||
layer.fabricObjects.push(
|
||||
this.textObject.toObject(["id", "layerId", "layerName"])
|
||||
);
|
||||
}
|
||||
|
||||
// 现在可以安全地设置为活动图层
|
||||
this.layerManager.setActiveLayer(this.layerId);
|
||||
|
||||
// 更新对象交互性
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
|
||||
// 切换到选择工具
|
||||
this.layerManager?.toolManager?.setTool?.(OperationType.SELECT);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`✅ 文本对象已创建: "${finalOptions.text}",位置: (${this.x}, ${this.y})`
|
||||
);
|
||||
return this.textObject;
|
||||
} catch (error) {
|
||||
console.error("创建文本对象失败:", error);
|
||||
// 如果创建失败,需要清理已创建的资源
|
||||
await this.undo();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能插入图层到正确位置
|
||||
* 根据当前激活图层位置确定新图层插入位置
|
||||
* @param {Object} newLayer 要插入的新图层
|
||||
* @private
|
||||
*/
|
||||
_insertLayerAtCorrectPosition(newLayer) {
|
||||
const layers = this.layerManager.layers.value;
|
||||
const currentActiveLayerId = this.layerManager.activeLayerId?.value;
|
||||
|
||||
// 如果没有当前激活图层,插入到顶部(索引0)
|
||||
if (!currentActiveLayerId) {
|
||||
layers.splice(0, 0, newLayer);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找当前激活图层的位置
|
||||
const {
|
||||
layer: activeLayer,
|
||||
parent: parentLayer,
|
||||
index: activeIndex,
|
||||
} = this._findLayerPosition(currentActiveLayerId);
|
||||
|
||||
if (!activeLayer) {
|
||||
// 没找到激活图层,插入到顶部
|
||||
layers.splice(0, 0, newLayer);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确定插入位置
|
||||
let insertIndex = 0;
|
||||
|
||||
if (parentLayer) {
|
||||
// 当前激活图层是子图层
|
||||
// 在同一父图层内,插入到激活子图层之上
|
||||
insertIndex = Math.max(0, activeIndex);
|
||||
parentLayer.children = parentLayer.children || [];
|
||||
parentLayer.children.splice(insertIndex, 0, newLayer);
|
||||
|
||||
console.log(
|
||||
`新图层已插入到子图层位置: ${insertIndex} (父图层: ${parentLayer.name})`
|
||||
);
|
||||
} else {
|
||||
// 当前激活图层是一级图层
|
||||
// 在一级图层中,插入到激活图层之上
|
||||
const activeLayerIndex = layers.findIndex(
|
||||
(layer) => layer.id === currentActiveLayerId
|
||||
);
|
||||
insertIndex = Math.max(0, activeLayerIndex);
|
||||
layers.splice(insertIndex, 0, newLayer);
|
||||
|
||||
console.log(`新图层已插入到一级图层位置: ${insertIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找图层位置信息
|
||||
* @param {String} layerId 图层ID
|
||||
* @returns {Object} 包含图层、父图层和索引的对象
|
||||
* @private
|
||||
*/
|
||||
_findLayerPosition(layerId) {
|
||||
const layers = this.layerManager.layers.value;
|
||||
|
||||
// 先在一级图层中查找
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
|
||||
if (layer.id === layerId) {
|
||||
return {
|
||||
layer: layer,
|
||||
parent: null,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
|
||||
// 在子图层中查找
|
||||
if (layer.children && Array.isArray(layer.children)) {
|
||||
for (let j = 0; j < layer.children.length; j++) {
|
||||
const childLayer = layer.children[j];
|
||||
if (childLayer.id === layerId) {
|
||||
return {
|
||||
layer: childLayer,
|
||||
parent: layer,
|
||||
index: j,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
layer: null,
|
||||
parent: null,
|
||||
index: -1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {String} 唯一ID
|
||||
*/
|
||||
generateId() {
|
||||
return `text_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
textId: this.textObject?.id,
|
||||
layerId: this.layerId,
|
||||
text: this.textOptions.text || this.defaultOptions.text,
|
||||
position: { x: this.x, y: this.y },
|
||||
};
|
||||
}
|
||||
|
||||
async undo() {
|
||||
try {
|
||||
// 从画布移除文本对象
|
||||
if (this.textObject && this.canvas) {
|
||||
this.canvas.remove(this.textObject);
|
||||
}
|
||||
|
||||
// 智能移除创建的图层
|
||||
if (this.layerId && this.layerManager) {
|
||||
this._removeLayerFromCorrectPosition();
|
||||
}
|
||||
|
||||
// 恢复原活动图层
|
||||
if (this.oldActiveLayerId && this.layerManager) {
|
||||
// 检查原活动图层是否还存在
|
||||
const originalLayer = this.layerManager.getLayerById(
|
||||
this.oldActiveLayerId
|
||||
);
|
||||
if (originalLayer) {
|
||||
this.layerManager.setActiveLayer(this.oldActiveLayerId);
|
||||
} else {
|
||||
// 如果原图层不存在,设置为第一个可用的普通图层
|
||||
const availableLayers = this.layerManager.layers.value.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed && !layer.locked
|
||||
);
|
||||
if (availableLayers.length > 0) {
|
||||
this.layerManager.setActiveLayer(availableLayers[0].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新对象交互性
|
||||
await this.layerManager.updateLayersObjectsInteractivity();
|
||||
|
||||
// 重新渲染画布
|
||||
if (this.canvas) {
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
console.log(`↩️ 文本创建操作已撤销`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("撤销文本创建操作失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能移除图层
|
||||
* 根据图层位置(一级图层或子图层)进行相应的移除操作
|
||||
* @private
|
||||
*/
|
||||
_removeLayerFromCorrectPosition() {
|
||||
const layers = this.layerManager.layers.value;
|
||||
|
||||
// 查找图层位置信息
|
||||
const positionInfo = this._findLayerPosition(this.layerId);
|
||||
|
||||
if (!positionInfo.layer) {
|
||||
console.warn(`要移除的图层不存在: ${this.layerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (positionInfo.parent) {
|
||||
// 从子图层中移除
|
||||
if (positionInfo.parent.children && positionInfo.index >= 0) {
|
||||
positionInfo.parent.children.splice(positionInfo.index, 1);
|
||||
console.log(
|
||||
`已从子图层移除: ${this.layerId} (父图层: ${positionInfo.parent.name})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 从一级图层中移除
|
||||
if (positionInfo.index >= 0) {
|
||||
layers.splice(positionInfo.index, 1);
|
||||
console.log(`已从一级图层移除: ${this.layerId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ import { BrushStore } from "../store/BrushStore";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { inject } from "vue";
|
||||
import VerticalSlider from "./VerticalSlider.vue";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue"
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: {
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import BrushPanel from "./BrushPanel.vue";
|
||||
import { BrushStore } from "../store/BrushStore";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue"
|
||||
|
||||
// 提供brushStore给子组件
|
||||
provide("brushStore", BrushStore);
|
||||
|
||||
@@ -47,30 +45,34 @@ const brushPanelRef = ref(null);
|
||||
// return props.activeTool === OperationType.DRAW;
|
||||
// });
|
||||
|
||||
function updateCanvasSize() {
|
||||
function updateCanvasSize(
|
||||
{ width, height } = { width: props.width, height: props.height }
|
||||
) {
|
||||
if (!layerManager) {
|
||||
console.warn("LayerManager 未初始化,无法调整背景层尺寸");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查画布上是否有除了背景层的其他元素
|
||||
const hasOtherElements = layerManager.layers.value.some((layer) => {
|
||||
if (layer.isBackground) return false;
|
||||
// 检查普通图层是否有对象
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) return true;
|
||||
// 检查固定图层是否有对象
|
||||
if (layer.isFixed && layer.fabricObjects && layer.fabricObjects.length > 0)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
layerManager.resizeCanvas(width, height);
|
||||
|
||||
if (hasOtherElements) {
|
||||
// 有其他元素时使用等比缩放命令
|
||||
layerManager.resizeCanvasWithScale(props.canvasWidth, props.canvasHeight);
|
||||
} else {
|
||||
// 只有背景层时使用普通调整命令
|
||||
layerManager.resizeCanvas(props.canvasWidth, props.canvasHeight);
|
||||
}
|
||||
// // 检查画布上是否有除了背景层的其他元素
|
||||
// const hasOtherElements = layerManager.layers.value.some((layer) => {
|
||||
// if (layer.isBackground) return false;
|
||||
// // 检查普通图层是否有对象
|
||||
// if (layer.fabricObjects && layer.fabricObjects.length > 0) return true;
|
||||
// // 检查固定图层是否有对象
|
||||
// if (layer.isFixed && layer.fabricObjects && layer.fabricObjects.length > 0)
|
||||
// return true;
|
||||
// return false;
|
||||
// });
|
||||
|
||||
// if (hasOtherElements) {
|
||||
// // 有其他元素时使用等比缩放命令
|
||||
// layerManager.resizeCanvasWithScale(width, height);
|
||||
// } else {
|
||||
// // 只有背景层时使用普通调整命令
|
||||
// layerManager.resizeCanvas(width, height);
|
||||
// }
|
||||
|
||||
emit("canvas-size-change");
|
||||
}
|
||||
@@ -268,27 +270,39 @@ onMounted(() => {
|
||||
" -->
|
||||
<div class="canvas-settings" v-if="!props.enabledRedGreenMode">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Width</span>
|
||||
<input
|
||||
type="text"
|
||||
<span class="setting-label">{{ $t("宽度") }}</span>
|
||||
<a-input-number
|
||||
:value="canvasWidth"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
:step="1"
|
||||
@change="
|
||||
(value) => {
|
||||
$emit('update:canvasWidth', value);
|
||||
updateCanvasSize({ width: value, height: canvasHeight });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Height</span>
|
||||
<input
|
||||
type="text"
|
||||
<span class="setting-label">{{ $t("高度") }}</span>
|
||||
<a-input-number
|
||||
:value="canvasHeight"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
:step="1"
|
||||
@change="
|
||||
(value) => {
|
||||
$emit('update:canvasHeight', value);
|
||||
updateCanvasSize({ width: canvasWidth, height: value });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color</span>
|
||||
<span class="setting-label">{{ $t("颜色") }}</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
@@ -323,11 +337,12 @@ onMounted(() => {
|
||||
</div> -->
|
||||
|
||||
<!-- 绘图工具设置 -->
|
||||
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
|
||||
<div class="canvas-settings gap-20">
|
||||
<div
|
||||
class="btn"
|
||||
:class="{ active: showBrushPanel }"
|
||||
@click="toggleBrushPanel"
|
||||
v-if="!props.enabledRedGreenMode"
|
||||
>
|
||||
<!-- <span class="setting-label">笔刷:</span>/ -->
|
||||
<div class="brush-selector">
|
||||
@@ -439,10 +454,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
width: 60px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.setting-input :deep(.ant-input-number-input) {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -494,15 +510,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.brush-selector {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// border: 1px solid #ddd;
|
||||
// border-radius: 4px;
|
||||
// padding: 5px;
|
||||
// cursor: pointer;
|
||||
// background-color: white;
|
||||
// width: 80px;
|
||||
// justify-content: space-between;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.brush-preview {
|
||||
|
||||
@@ -43,10 +43,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
thumbnailUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
// thumbnailUrl: {
|
||||
// type: String,
|
||||
// default: null,
|
||||
// },
|
||||
isHidenDragHandle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -404,14 +404,11 @@ function findParentLayerId() {
|
||||
<!-- 图层预览图标 -->
|
||||
<div class="layer-review">
|
||||
<img
|
||||
v-if="thumbnailUrl"
|
||||
:src="thumbnailUrl"
|
||||
v-if="layer.thumbnailUrl"
|
||||
:src="layer.thumbnailUrl"
|
||||
class="layer-thumbnail"
|
||||
:alt="$t('图层预览')"
|
||||
/>
|
||||
<span v-else class="layer-type-icon">{{
|
||||
getLayerTypeIcon(layer)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 图层名称 -->
|
||||
|
||||
@@ -119,6 +119,8 @@ const canDeleteComputed = computed(() => {
|
||||
<template v-for="(layer, index) in sortableRootLayers" :key="layer.id">
|
||||
<div class="layer-group">
|
||||
<!-- 使用 LayerItem 子组件 -->
|
||||
<!-- :thumbnail-url="getLayerThumbnail(layer.id)" -->
|
||||
|
||||
<LayerItem
|
||||
:layer="layer"
|
||||
:is-child="isChild"
|
||||
@@ -133,7 +135,6 @@ const canDeleteComputed = computed(() => {
|
||||
!layer.isFixed &&
|
||||
!layer.locked
|
||||
"
|
||||
:thumbnail-url="getLayerThumbnail(layer.id)"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
@click="(...args) => forwardEvent('layer-click', ...args)"
|
||||
@double-click="
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref, nextTick, inject } from "vue";
|
||||
import { isGroupLayer } from "../../utils/layerHelper";
|
||||
import { findLayerRecursively, isGroupLayer } from "../../utils/layerHelper";
|
||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
||||
import ContextMenu from "./ContextMenu.vue";
|
||||
import LayerItem from "./LayerItem.vue";
|
||||
@@ -691,15 +691,15 @@ function buildContextMenuItems(layer) {
|
||||
hideContextMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "合并组",
|
||||
icon: "CMergeGroup",
|
||||
disabled: !isGroupLayer || isMultiple,
|
||||
action: () => {
|
||||
mergeGroupLayer(layer.id);
|
||||
hideContextMenu();
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: "合并组", // 不需要了 同栅格化功能一样了
|
||||
// icon: "CMergeGroup",
|
||||
// disabled: !isGroupLayer || isMultiple,
|
||||
// action: () => {
|
||||
// mergeGroupLayer(layer.id);
|
||||
// hideContextMenu();
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
// 图层操作 - 带子菜单
|
||||
@@ -1099,8 +1099,7 @@ async function rasterizeLayer(layerId) {
|
||||
try {
|
||||
const success = await layerManager.rasterizeLayer(layerId);
|
||||
if (success) {
|
||||
const layer = layers.value.find((l) => l.id === layerId);
|
||||
console.log(`✅ 成功栅格化图层: ${layer?.name || layerId}`);
|
||||
console.log(`✅ 成功栅格化图层: ${layerId}`);
|
||||
} else {
|
||||
console.warn("栅格化图层失败");
|
||||
}
|
||||
@@ -1164,6 +1163,11 @@ function moveLayerToBottom(layerId) {
|
||||
console.warn("置底图层失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 事件转发方法
|
||||
const forwardEvent = (eventName, ...args) => {
|
||||
emit(eventName, ...args);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1281,6 +1285,8 @@ function moveLayerToBottom(layerId) {
|
||||
<!-- 固定层(背景层和固定层) -->
|
||||
<div v-if="fixedLayers.length > 0" class="fixed-layers">
|
||||
<!-- 遍历固定层 -->
|
||||
<!-- :thumbnail-url="getLayerThumbnail(layer.id)" -->
|
||||
|
||||
<LayerItem
|
||||
v-for="layer in fixedLayers"
|
||||
:key="layer.id"
|
||||
@@ -1292,10 +1298,9 @@ function moveLayerToBottom(layerId) {
|
||||
:is-editing="editingLayerId === layer.id"
|
||||
:editing-name="editingLayerName"
|
||||
:can-delete="false"
|
||||
:thumbnail-url="getLayerThumbnail(layer.id)"
|
||||
:isHidenDragHandle="true"
|
||||
@toggle-visibility="
|
||||
(...args) => forwardEvent('toggle-visibility', ...args)
|
||||
(...args) => forwardEvent('toggle-layer-visibility', ...args)
|
||||
"
|
||||
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
|
||||
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
border-bottom: 1px solid #f5f2f2;
|
||||
padding-left: 30px;
|
||||
padding-right: 10px;
|
||||
color: #333;
|
||||
|
||||
&.group-layer {
|
||||
background-color: rgba(240, 248, 255, 0.3);
|
||||
@@ -677,6 +678,7 @@
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 4px;
|
||||
color: #333;
|
||||
|
||||
// &:hover {
|
||||
// background-color: rgba(0, 0, 0, 0.1);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue"
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
shallowRef,
|
||||
provide,
|
||||
defineExpose,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { CanvasManager } from "./managers/CanvasManager";
|
||||
import { LayerManager } from "./managers/LayerManager";
|
||||
@@ -28,7 +29,7 @@ import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本
|
||||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||
import { OperationType } from "./utils/layerHelper.js";
|
||||
import { ToolManager } from "./managers/ToolManager.js";
|
||||
import { ToolManager } from "./managers/toolManager.js";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { uploadImageAndCreateLayer } from "./utils/imageHelper.js";
|
||||
// import MinimapPanel from "./components/MinimapPanel.vue";
|
||||
@@ -59,6 +60,15 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "", // 红绿图URL
|
||||
},
|
||||
clothingImageOpts: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 引用和状态
|
||||
@@ -243,11 +253,10 @@ onMounted(async () => {
|
||||
// 初始化小地图
|
||||
// minimapManager.value = new MinimapManager(canvasManager.canvas);
|
||||
|
||||
// setTimeout(() => {
|
||||
// // historyManager.saveCanvasState();
|
||||
// // 初始状态下生成所有预览图
|
||||
// canvasManager.updateAllThumbnails();
|
||||
// }, 500);
|
||||
setTimeout(() => {
|
||||
// 初始状态下生成所有预览图
|
||||
canvasManager.updateAllThumbnails();
|
||||
}, 300);
|
||||
|
||||
// 使用window的resize事件代替ResizeObserver
|
||||
// 只有当窗口大小变化时才更新画布尺寸
|
||||
@@ -306,12 +315,38 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果初始化有默认底图,设置底图 - 红绿图模式不通过初始化重置底图了
|
||||
if (!isRedGreenMode.value && props.clothingImageUrl) {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
|
||||
undoable: false, // 不可撤销操作
|
||||
...(props?.clothingImageOpts || {}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("更换底图失败:", error);
|
||||
}
|
||||
}, 92); // 延迟 确保更新底图完成
|
||||
});
|
||||
|
||||
this.canvasManager?.centerBackgroundLayer?.(
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
}
|
||||
|
||||
// 初始设置
|
||||
handleWindowResize(); // 设置画布大小
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (import.meta.hot) {
|
||||
// 热更新 ?
|
||||
console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
||||
return; // 开发环境下不卸载组件
|
||||
}
|
||||
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
||||
canvasManager?.dispose?.();
|
||||
commandManager?.dispose?.();
|
||||
@@ -397,8 +432,8 @@ function updateCanvasColor() {
|
||||
canvasManager.setCanvasColor(canvasColor.value);
|
||||
}
|
||||
|
||||
function addLayer() {
|
||||
layerManager.createLayer();
|
||||
async function addLayer() {
|
||||
await layerManager.createLayer();
|
||||
}
|
||||
|
||||
function setActiveLayer(layerId) {
|
||||
@@ -815,12 +850,9 @@ defineExpose({
|
||||
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="layers-panel"
|
||||
v-if="isShowLayerPanel && !enabledRedGreenMode"
|
||||
>
|
||||
<div class="layers-panel" v-if="isShowLayerPanel">
|
||||
<LayersPanel
|
||||
v-if="canvasManagerLoaded && canvasManager?.canvas"
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeLayerId="activeLayerId"
|
||||
:activeElementId="activeElementId"
|
||||
:thumbnailManager="canvasManager.thumbnailManager"
|
||||
|
||||
@@ -66,11 +66,13 @@ export class CanvasManager {
|
||||
// 初始化缩略图管理器
|
||||
this.thumbnailManager = new ThumbnailManager(this.canvas, {
|
||||
// 可以根据需求自定义选项
|
||||
layerThumbSize: { width: 32, height: 32 },
|
||||
elementThumbSize: { width: 32, height: 24 },
|
||||
// layerThumbSize: { width: 32, height: 32 },
|
||||
// elementThumbSize: { width: 32, height: 24 },
|
||||
layers: this.layers,
|
||||
});
|
||||
|
||||
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
|
||||
|
||||
// 设置画布辅助线
|
||||
initAligningGuidelines(this.canvas);
|
||||
|
||||
@@ -110,6 +112,8 @@ export class CanvasManager {
|
||||
activeLayer,
|
||||
});
|
||||
|
||||
this.thumbnailManager?.generateLayerThumbnail(activeLayer.id);
|
||||
|
||||
// 返回true表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return true;
|
||||
} else {
|
||||
@@ -147,6 +151,10 @@ export class CanvasManager {
|
||||
// 更新交互性
|
||||
command &&
|
||||
(await this.layerManager?.updateLayersObjectsInteractivity?.());
|
||||
|
||||
this.thumbnailManager?.generateLayerThumbnail(
|
||||
this.layerManager?.activeLayerId?.value
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,9 +348,9 @@ export class CanvasManager {
|
||||
* 重置视图变换,使元素回到原始位置
|
||||
* @private
|
||||
*/
|
||||
_resetViewportTransform() {
|
||||
_resetViewportTransform(zoom) {
|
||||
// 保存当前缩放值
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
const currentZoom = zoom ?? this.canvas.getZoom();
|
||||
|
||||
// 重置视图变换,但保留缩放级别
|
||||
this.canvas.setViewportTransform([currentZoom, 0, 0, currentZoom, 0, 0]);
|
||||
@@ -370,6 +378,14 @@ export class CanvasManager {
|
||||
// 获取背景对象
|
||||
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
||||
|
||||
this.canvas?.clipPath?.set?.({
|
||||
left: this.width / 2,
|
||||
top: this.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
this.canvas?.clipPath?.setCoords?.();
|
||||
// 如果只有背景层或没有背景层,使用原有逻辑
|
||||
if (!backgroundObject) {
|
||||
console.warn("未找到背景层,使用默认居中逻辑");
|
||||
@@ -462,9 +478,12 @@ export class CanvasManager {
|
||||
|
||||
setCanvasColor(color) {
|
||||
this.backgroundColor = color;
|
||||
this.canvas.setBackgroundColor(
|
||||
color,
|
||||
this.canvas.renderAll.bind(this.canvas)
|
||||
// this.canvas.setBackgroundColor(
|
||||
// color,
|
||||
// this.canvas.renderAll.bind(this.canvas)
|
||||
// );
|
||||
this.thumbnailManager?.generateLayerThumbnail?.(
|
||||
this.layers?.value.find((layer) => layer.isBackground)?.id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -615,80 +634,15 @@ export class CanvasManager {
|
||||
* @param {String} layerId 图层ID
|
||||
*/
|
||||
updateLayerThumbnail(layerId) {
|
||||
if (!this.thumbnailManager || !layerId || !this.layers) return;
|
||||
|
||||
const layer = this.layers.value.find((l) => l.id === layerId);
|
||||
if (layer) {
|
||||
this.thumbnailManager.generateLayerThumbnail(layer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定元素图层的缩略图
|
||||
* @param {String} elementId 元素ID
|
||||
* @param {Object} fabricObject fabric对象
|
||||
*/
|
||||
updateElementThumbnail(elementId, fabricObject) {
|
||||
if (this.eventManager) {
|
||||
this.eventManager.updateElementThumbnail(elementId, fabricObject);
|
||||
} else if (
|
||||
this.thumbnailManager &&
|
||||
elementId &&
|
||||
fabricObject &&
|
||||
this.layers
|
||||
) {
|
||||
// 查找对应的图层(现在元素就是图层)
|
||||
const layer = this.layers.value.find(
|
||||
(l) =>
|
||||
l.id === elementId ||
|
||||
(l.fabricObject && l.fabricObject.id === elementId)
|
||||
);
|
||||
|
||||
if (layer) {
|
||||
// 生成图层缩略图
|
||||
this.thumbnailManager.generateLayerThumbnail(layer);
|
||||
}
|
||||
|
||||
// 同时也维护元素缩略图,以保持向后兼容性
|
||||
this.thumbnailManager.generateElementThumbnail(
|
||||
{ id: elementId, type: fabricObject.type },
|
||||
fabricObject
|
||||
);
|
||||
}
|
||||
this.thumbnailManager?.generateLayerThumbnail?.(layerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有图层和元素的缩略图
|
||||
*/
|
||||
updateAllThumbnails() {
|
||||
if (!this.thumbnailManager || !this.layers) return;
|
||||
|
||||
this.thumbnailManager.generateAllLayerThumbnails(this.layers.value);
|
||||
|
||||
// 为所有元素生成缩略图
|
||||
this.layers.value.forEach((layer) => {
|
||||
// 如果是分组图层,处理子图层
|
||||
if (isGroupLayer(layer) && layer.children) {
|
||||
layer.children.forEach((childLayerId) => {
|
||||
const childLayer = this.layers.value.find(
|
||||
(l) => l.id === childLayerId
|
||||
);
|
||||
if (childLayer && childLayer.fabricObject) {
|
||||
this.thumbnailManager.generateElementThumbnail(
|
||||
{ id: childLayer.id, type: childLayer.fabricObject.type },
|
||||
childLayer.fabricObject
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 如果是元素图层,则直接生成缩略图
|
||||
else if (layer.isElementLayer && layer.fabricObject) {
|
||||
this.thumbnailManager.generateElementThumbnail(
|
||||
{ id: layer.id, type: layer.fabricObject.type },
|
||||
layer.fabricObject
|
||||
);
|
||||
}
|
||||
});
|
||||
this.thumbnailManager?.generateAllLayerThumbnails?.(this.layers.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -710,6 +664,7 @@ export class CanvasManager {
|
||||
layerManager: this.layerManager,
|
||||
imageUrl: imageUrl,
|
||||
targetLayerType: options.targetLayerType || "fixed", // background/fixed
|
||||
options: options,
|
||||
});
|
||||
|
||||
command.undoable =
|
||||
@@ -812,110 +767,72 @@ export class CanvasManager {
|
||||
|
||||
getJSON() {
|
||||
// // 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
// let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 创建对象ID映射表,用于快速查找
|
||||
// tempLayers = tempLayers.map((layer) => {
|
||||
// const newLayer = { ...layer };
|
||||
|
||||
// // 处理fabricObjects数组
|
||||
// if (Array.isArray(layer.fabricObjects)) {
|
||||
// newLayer.fabricObjects = layer.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// // 确保对象有ID
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
// const simplifyLayers = (layers) => {
|
||||
// return layers.map((layer) => {
|
||||
// if (layer?.children?.length) {
|
||||
// layer.children = layer.children.map((child) => {
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object", // 保存类型信息用于调试
|
||||
// id: child.id,
|
||||
// type: child.type,
|
||||
// layerId: child.layerId,
|
||||
// layerName: child.layerName,
|
||||
// isBackground: child.isBackground,
|
||||
// isLocked: child.isLocked,
|
||||
// isVisible: child.isVisible,
|
||||
// isFixed: child.isFixed,
|
||||
// parentId: child.parentId,
|
||||
// fabricObject: child.fabricObject
|
||||
// ? {
|
||||
// id: child.fabricObject.id,
|
||||
// type: child.fabricObject.type,
|
||||
// layerId: child.fabricObject.layerId,
|
||||
// layerName: child.fabricObject.layerName,
|
||||
// }
|
||||
// : {},
|
||||
// fabricObjects:
|
||||
// child.fabricObjects?.map((obj) => ({
|
||||
// id: obj.id,
|
||||
// type: obj.type,
|
||||
// layerId: obj.layerId,
|
||||
// layerName: obj.layerName,
|
||||
// })) || [],
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newLayer.fabricObjects = [];
|
||||
// }
|
||||
|
||||
// if (layer.clippingMask) {
|
||||
// layer.clippingMask = {
|
||||
// id: layer.clippingMask.id,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // 处理单个fabricObject
|
||||
// if (layer.fabricObject) {
|
||||
// if (!layer.fabricObject.id) {
|
||||
// layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// });
|
||||
// }
|
||||
// newLayer.fabricObject = {
|
||||
// id: layer.fabricObject.id,
|
||||
// type: layer.fabricObject.type || "object",
|
||||
// return {
|
||||
// id: layer.id,
|
||||
// type: layer.type,
|
||||
// layerId: layer.layerId,
|
||||
// layerName: layer.layerName,
|
||||
// isBackground: layer.isBackground,
|
||||
// isLocked: layer.isLocked,
|
||||
// isVisible: layer.isVisible,
|
||||
// isFixed: layer.isFixed,
|
||||
// parentId: layer.parentId,
|
||||
// fabricObject: child.fabricObject
|
||||
// ? {
|
||||
// id: child.fabricObject.id,
|
||||
// type: child.fabricObject.type,
|
||||
// layerId: child.fabricObject.layerId,
|
||||
// layerName: child.fabricObject.layerName,
|
||||
// }
|
||||
// : {},
|
||||
// fabricObjects:
|
||||
// child.fabricObjects?.map((obj) => ({
|
||||
// id: obj.id,
|
||||
// type: obj.type,
|
||||
// layerId: obj.layerId,
|
||||
// layerName: obj.layerName,
|
||||
// })) || [],
|
||||
// children: layer.children,
|
||||
// };
|
||||
// } else {
|
||||
// newLayer.fabricObject = null;
|
||||
// }
|
||||
|
||||
// // 处理子图层
|
||||
// if (Array.isArray(layer.children)) {
|
||||
// newLayer.children = layer.children.map((cItem) => {
|
||||
// const newChild = { ...cItem };
|
||||
|
||||
// // 处理子图层的fabricObjects
|
||||
// if (Array.isArray(cItem.fabricObjects)) {
|
||||
// newChild.fabricObjects = cItem.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object",
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newChild.fabricObjects = [];
|
||||
// }
|
||||
|
||||
// // 处理子图层的fabricObject
|
||||
// if (cItem.fabricObject) {
|
||||
// if (!cItem.fabricObject.id) {
|
||||
// cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newChild.fabricObject = {
|
||||
// id: cItem.fabricObject.id,
|
||||
// type: cItem.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newChild.fabricObject = null;
|
||||
// }
|
||||
|
||||
// return newChild;
|
||||
// });
|
||||
// } else {
|
||||
// newLayer.children = [];
|
||||
// }
|
||||
|
||||
// return newLayer;
|
||||
// });
|
||||
// });
|
||||
// };
|
||||
try {
|
||||
console.log(
|
||||
"获取画布JSON数据...",
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
const simplifyLayersData = simplifyLayers(
|
||||
JSON.parse(JSON.stringify(this.layers.value))
|
||||
);
|
||||
console.log("获取画布JSON数据...", simplifyLayersData);
|
||||
return JSON.stringify({
|
||||
canvas: this.canvas.toJSON([
|
||||
"id",
|
||||
@@ -931,15 +848,14 @@ export class CanvasManager {
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]),
|
||||
layers: JSON.stringify(
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
), // 简化图层数据
|
||||
layers: JSON.stringify(simplifyLayersData), // 简化图层数据
|
||||
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
|
||||
version: "1.0", // 添加版本信息
|
||||
timestamp: new Date().toISOString(), // 添加时间戳
|
||||
canvasWidth: this.canvasWidth.value,
|
||||
canvasHeight: this.canvasHeight.value,
|
||||
canvasColor: this.canvasColor.value,
|
||||
activeLayerId: this.canvas.activeLayerId.value,
|
||||
activeLayerId: this.layerManager?.activeLayerId?.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取画布JSON失败:", error);
|
||||
@@ -960,7 +876,7 @@ export class CanvasManager {
|
||||
try {
|
||||
const parsedJson = JSON.parse(json);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const tempLayers = JSON.parse(parsedJson?.layers) || [];
|
||||
const canvasData = parsedJson?.canvas;
|
||||
|
||||
@@ -983,19 +899,43 @@ export class CanvasManager {
|
||||
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
|
||||
|
||||
// 重置视图变换以确保元素位置正确
|
||||
this._resetViewportTransform();
|
||||
this._resetViewportTransform(1);
|
||||
let canvasClipPath = null;
|
||||
// 克隆当前裁剪路径
|
||||
if (this.canvas?.clipPath) {
|
||||
canvasClipPath = this.canvas?.clipPath;
|
||||
}
|
||||
|
||||
// 清除当前画布内容
|
||||
this.canvas.clear();
|
||||
|
||||
console.log("清除当前画布内容", canvasData);
|
||||
delete canvasData.clipPath; // 删除当前裁剪路径
|
||||
// 加载画布数据
|
||||
this.canvas.loadFromJSON(canvasData, async () => {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 清空重做栈
|
||||
this.commandManager?.clear?.();
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
if (canvasClipPath) {
|
||||
// canvasClipPath.set({
|
||||
// absolutePositioned: true,
|
||||
// });
|
||||
this.canvas.clipPath = canvasClipPath;
|
||||
// await new Promise((resolve) => {
|
||||
// debugger;
|
||||
// fabric.util.enlivenObjects([canvasClipPath], (clipPaths) => {
|
||||
// if (clipPaths && clipPaths.length > 0) {
|
||||
// resolve(clipPaths[0]);
|
||||
// } else {
|
||||
// resolve(null);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// debugger;
|
||||
}
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
|
||||
// 重新构建对象关系
|
||||
restoreObjectLayerAssociations(
|
||||
this.layers.value,
|
||||
@@ -1010,8 +950,8 @@ export class CanvasManager {
|
||||
|
||||
console.log("图层关联验证结果:", isValidate);
|
||||
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
this.layerManager.activeLayerId.value =
|
||||
this.layers.value[0]?.id || parsedJson?.activeLayerId || null;
|
||||
|
||||
// // 如果检测到红绿图模式内容,进行缩放调整
|
||||
// if (this.enabledRedGreenMode) {
|
||||
@@ -1026,12 +966,11 @@ export class CanvasManager {
|
||||
false
|
||||
);
|
||||
console.log(this.layerManager.layers.value);
|
||||
debugger;
|
||||
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
}, 500);
|
||||
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
|
||||
@@ -225,16 +225,106 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图层顺序收集对象(从底到顶)
|
||||
* 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象)
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {Array} 画布中的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsFromLayer(layer) {
|
||||
if (!layer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const realObjects = [];
|
||||
|
||||
// 收集当前图层的对象
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
for (const layerObj of layer.fabricObjects) {
|
||||
if (!layerObj || !layerObj.id) continue;
|
||||
|
||||
// 通过ID在画布中查找真实对象
|
||||
const realObj = this._findRealObjectById(layerObj.id);
|
||||
if (realObj && realObj.visible !== false) {
|
||||
realObjects.push(realObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归收集子图层的对象
|
||||
if (layer.children && layer.children.length > 0) {
|
||||
for (const childLayer of layer.children) {
|
||||
const childObjects = this._collectObjectsFromLayer(childLayer);
|
||||
realObjects.push(...childObjects);
|
||||
}
|
||||
}
|
||||
|
||||
return realObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ID在画布中查找真实对象
|
||||
* @param {String} objectId 对象ID
|
||||
* @returns {Object|null} 画布中的真实对象
|
||||
* @private
|
||||
*/
|
||||
_findRealObjectById(objectId) {
|
||||
if (!objectId || !this.canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用helper工具查找对象
|
||||
const result = findObjectById(this.canvas, objectId);
|
||||
return result?.object || null;
|
||||
} catch (error) {
|
||||
console.warn(`查找对象 ${objectId} 失败:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出对象
|
||||
* @param {Object} obj fabric对象
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {String} 图片数据URL
|
||||
* @private
|
||||
*/
|
||||
async _exportObject(
|
||||
obj,
|
||||
expPicType,
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen
|
||||
) {
|
||||
// 红绿图模式下使用固定尺寸和裁剪
|
||||
if (isRedGreenMode) {
|
||||
return this._exportWithRedGreenMode(
|
||||
[obj],
|
||||
expPicType,
|
||||
restoreOpacityInRedGreen
|
||||
);
|
||||
}
|
||||
|
||||
// 普通模式使用画布尺寸
|
||||
return this._exportWithCanvasSize(
|
||||
[obj],
|
||||
expPicType,
|
||||
restoreOpacityInRedGreen
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图层顺序收集对象(优化版本 - 从底到顶)
|
||||
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @returns {Array} 按正确顺序排列的对象数组
|
||||
* @returns {Array} 按正确顺序排列的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
|
||||
const objectsToExport = [];
|
||||
const allLayers = this._getAllLayers();
|
||||
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
|
||||
|
||||
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
||||
for (let i = allLayers.length - 1; i >= 0; i--) {
|
||||
@@ -256,6 +346,126 @@ export class ExportManager {
|
||||
return objectsToExport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扁平化的图层列表(包含子图层)
|
||||
* @returns {Array} 扁平化的图层数组
|
||||
* @private
|
||||
*/
|
||||
_getAllLayersFlattened() {
|
||||
const flattenedLayers = [];
|
||||
const rootLayers = this._getAllLayers();
|
||||
|
||||
const flattenLayer = (layer) => {
|
||||
flattenedLayers.push(layer);
|
||||
|
||||
// 递归处理子图层
|
||||
if (layer.children && layer.children.length > 0) {
|
||||
for (const childLayer of layer.children) {
|
||||
flattenLayer(childLayer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理所有根图层
|
||||
for (const layer of rootLayers) {
|
||||
flattenLayer(layer);
|
||||
}
|
||||
|
||||
return flattenedLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算对象组的边界
|
||||
* @param {Array} objects 对象数组
|
||||
* @returns {Object} 边界信息 {left, top, width, height}
|
||||
* @private
|
||||
*/
|
||||
_calculateGroupBounds(objects) {
|
||||
if (!objects || objects.length === 0) {
|
||||
return { left: 0, top: 0, width: 1, height: 1 };
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
if (!obj || typeof obj.getBoundingRect !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = obj.getBoundingRect();
|
||||
minX = Math.min(minX, bounds.left);
|
||||
minY = Math.min(minY, bounds.top);
|
||||
maxX = Math.max(maxX, bounds.left + bounds.width);
|
||||
maxY = Math.max(maxY, bounds.top + bounds.height);
|
||||
});
|
||||
|
||||
if (minX === Infinity || minY === Infinity) {
|
||||
return { left: 0, top: 0, width: 1, height: 1 };
|
||||
}
|
||||
|
||||
// 添加小量边距避免边缘裁切
|
||||
const padding = 2;
|
||||
return {
|
||||
left: minX - padding,
|
||||
top: minY - padding,
|
||||
width: maxX - minX + padding * 2,
|
||||
height: maxY - minY + padding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象并添加到临时画布,调整位置偏移
|
||||
* @param {fabric.Canvas} tempCanvas 临时画布
|
||||
* @param {Object} obj 要克隆的对象
|
||||
* @param {Object} bounds 边界信息
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 是否恢复透明度
|
||||
* @returns {Promise<Object>} 克隆的对象
|
||||
* @private
|
||||
*/
|
||||
async _cloneAndAddObjectWithOffset(
|
||||
tempCanvas,
|
||||
obj,
|
||||
bounds,
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen
|
||||
) {
|
||||
try {
|
||||
const cloned = await this._cloneObjectForExport(
|
||||
obj,
|
||||
isRedGreenMode && restoreOpacityInRedGreen
|
||||
);
|
||||
|
||||
if (cloned) {
|
||||
// 获取对象当前边界
|
||||
const objBounds = obj.getBoundingRect();
|
||||
|
||||
// 计算相对于组边界的偏移
|
||||
const offsetX = objBounds.left - bounds.left;
|
||||
const offsetY = objBounds.top - bounds.top;
|
||||
|
||||
// 设置新位置(相对于临时画布的原点)
|
||||
cloned.set({
|
||||
left: offsetX + objBounds.width / 2,
|
||||
top: offsetY + objBounds.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
cloned.setCoords();
|
||||
tempCanvas.add(cloned);
|
||||
return cloned;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`克隆对象失败: ${obj?.id || "未知"}`, error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 红绿图模式导出(使用固定图层底图作为画布尺寸和裁剪区域)
|
||||
* @param {Array} objectsToExport 要导出的对象数组
|
||||
@@ -270,7 +480,8 @@ export class ExportManager {
|
||||
restoreOpacityInRedGreen
|
||||
) {
|
||||
// 获取固定图层对象(衣服底图)作为参考
|
||||
const fixedLayerObject = this._getFixedLayerObject();
|
||||
const fixedLayerObject =
|
||||
this._getFixedLayerObject() ?? this.canvas.clipPath;
|
||||
if (!fixedLayerObject) {
|
||||
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸");
|
||||
return this._exportWithCanvasSize(
|
||||
@@ -281,7 +492,7 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
|
||||
const fixedBounds = fixedLayerObject.getBoundingRect();
|
||||
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
|
||||
|
||||
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
||||
const canvasWidth = Math.round(fixedBounds.width);
|
||||
@@ -308,8 +519,8 @@ export class ExportManager {
|
||||
tempFabricCanvas.setZoom(1);
|
||||
|
||||
try {
|
||||
// 获取图层下标为1的对象作为裁剪路径
|
||||
const clipPathObject = await this._getLayerClipPathObject(1, fixedBounds);
|
||||
// 获取裁剪路径对象(如果存在)
|
||||
const clipPathObject = await this._getClipPathObject(fixedBounds);
|
||||
|
||||
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
||||
for (let i = 0; i < objectsToExport.length; i++) {
|
||||
@@ -429,7 +640,39 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象用于导出
|
||||
* 异步克隆fabric对象(参照createRasterizedImage的方法)
|
||||
* @param {fabric.Object} obj 要克隆的对象
|
||||
* @param {Array} propertiesToInclude 要包含的属性
|
||||
* @returns {Promise<fabric.Object>} 克隆的对象
|
||||
* @private
|
||||
*/
|
||||
_cloneObjectAsync(
|
||||
obj,
|
||||
propertiesToInclude = ["id", "layerId", "layerName", "name"]
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!obj) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
obj.clone((cloned) => {
|
||||
if (cloned) {
|
||||
resolve(cloned);
|
||||
} else {
|
||||
reject(new Error("对象克隆失败"));
|
||||
}
|
||||
}, propertiesToInclude);
|
||||
} catch (error) {
|
||||
console.warn("克隆对象失败:", error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象用于导出(优化版本)
|
||||
* @param {Object} obj fabric对象
|
||||
* @param {Boolean} forceRestoreOpacity 是否强制恢复透明度为1
|
||||
* @param {Boolean} removeClipPath 是否移除裁剪路径
|
||||
@@ -443,41 +686,169 @@ export class ExportManager {
|
||||
) {
|
||||
if (!obj) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
obj.clone(
|
||||
(cloned) => {
|
||||
if (cloned) {
|
||||
// 保持原始位置和属性
|
||||
cloned.set({
|
||||
selectable: false,
|
||||
evented: false,
|
||||
visible: true,
|
||||
});
|
||||
try {
|
||||
// 使用异步克隆方法
|
||||
const cloned = await this._cloneObjectAsync(obj);
|
||||
|
||||
// 如果需要恢复透明度
|
||||
if (forceRestoreOpacity) {
|
||||
cloned.set({ opacity: 1 });
|
||||
}
|
||||
if (cloned) {
|
||||
// 保持原始位置和属性
|
||||
cloned.set({
|
||||
selectable: false,
|
||||
evented: false,
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// 移除裁剪路径以避免绝对路径问题
|
||||
if (removeClipPath && cloned.clipPath) {
|
||||
console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`);
|
||||
cloned.clipPath = null;
|
||||
}
|
||||
// 如果需要恢复透明度
|
||||
if (forceRestoreOpacity) {
|
||||
cloned.set({ opacity: 1 });
|
||||
}
|
||||
|
||||
resolve(cloned);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
["id", "layerId", "layerName", "name"]
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("克隆对象失败:", error);
|
||||
resolve(null);
|
||||
// 移除裁剪路径以避免绝对路径问题
|
||||
if (removeClipPath && cloned.clipPath) {
|
||||
console.log(`移除对象 ${cloned.id || "未知"} 的裁剪路径`);
|
||||
cloned.clipPath = null;
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("克隆对象失败:", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出对象组
|
||||
* @param {Array} objectsToExport 要导出的对象数组
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {Promise<String>} 图片数据URL
|
||||
* @private
|
||||
*/
|
||||
async _exportObjectsAsGroup(
|
||||
objectsToExport,
|
||||
expPicType,
|
||||
isRedGreenMode = false,
|
||||
restoreOpacityInRedGreen = true
|
||||
) {
|
||||
if (!objectsToExport || objectsToExport.length === 0) {
|
||||
throw new Error("没有可导出的对象");
|
||||
}
|
||||
|
||||
// 计算所有对象的边界
|
||||
const bounds = this._calculateGroupBounds(objectsToExport);
|
||||
console.log("导出边界:", bounds);
|
||||
|
||||
// 创建高质量临时画布
|
||||
const scaleFactor = 2; // 高清导出
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = bounds.width * scaleFactor;
|
||||
tempCanvas.height = bounds.height * scaleFactor;
|
||||
tempCanvas.style.width = bounds.width + "px";
|
||||
tempCanvas.style.height = bounds.height + "px";
|
||||
|
||||
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
backgroundColor: null, // 透明背景
|
||||
});
|
||||
|
||||
// 启用高清缩放和图像平滑
|
||||
tempFabricCanvas.enableRetinaScaling = true;
|
||||
tempFabricCanvas.imageSmoothingEnabled = true;
|
||||
tempFabricCanvas.setZoom(scaleFactor);
|
||||
|
||||
try {
|
||||
// 克隆所有对象并添加到临时画布
|
||||
const clonedObjects = [];
|
||||
for (const obj of objectsToExport) {
|
||||
const cloned = await this._cloneAndAddObjectWithOffset(
|
||||
tempFabricCanvas,
|
||||
obj,
|
||||
bounds,
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen
|
||||
);
|
||||
if (cloned) {
|
||||
clonedObjects.push(cloned);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`成功克隆 ${clonedObjects.length} 个对象进行导出`);
|
||||
|
||||
// 渲染画布
|
||||
tempFabricCanvas.renderAll();
|
||||
|
||||
// 生成高质量数据URL
|
||||
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
||||
} finally {
|
||||
this._cleanupTempCanvas(tempFabricCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取裁剪路径对象(优化版本)
|
||||
* @param {Object} fixedBounds 固定图层边界
|
||||
* @returns {Promise<Object|null>} 裁剪路径对象
|
||||
* @private
|
||||
*/
|
||||
async _getClipPathObject(fixedBounds) {
|
||||
try {
|
||||
// const allLayers = this._getAllLayers();
|
||||
|
||||
// // 查找第一个有裁剪遮罩的图层
|
||||
// let clipObject = null;
|
||||
|
||||
// for (const layer of allLayers) {
|
||||
// if (layer.clippingMask?.id) {
|
||||
// const result = findObjectById(this.canvas, layer.clippingMask.id);
|
||||
// if (result?.object) {
|
||||
// clipObject = result.object;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const clipObject = this.canvas?.clipPath;
|
||||
if (!clipObject) {
|
||||
console.warn("未找到可用的裁剪对象");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 克隆对象作为裁剪路径
|
||||
const clonedClipPath = await this._cloneObjectForExport(
|
||||
clipObject,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if (!clonedClipPath) {
|
||||
console.warn("无法克隆裁剪对象");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 调整裁剪路径的位置相对于固定图层
|
||||
clonedClipPath.set({
|
||||
left: clonedClipPath.left - fixedBounds.left,
|
||||
top: clonedClipPath.top - fixedBounds.top,
|
||||
absolutePositioned: true, // 使用绝对定位
|
||||
});
|
||||
|
||||
// 更新坐标
|
||||
clonedClipPath.setCoords();
|
||||
|
||||
console.log("成功创建裁剪路径:", {
|
||||
objectType: clonedClipPath.type,
|
||||
position: { left: clonedClipPath.left, top: clonedClipPath.top },
|
||||
size: { width: clonedClipPath.width, height: clonedClipPath.height },
|
||||
});
|
||||
|
||||
return clonedClipPath;
|
||||
} catch (error) {
|
||||
console.error("获取裁剪路径失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -568,315 +939,27 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从图层收集对象
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {Array} 对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsFromLayer(layer) {
|
||||
if (!layer || !layer.fabricObjects) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return layer.fabricObjects.filter((obj) => obj && obj.visible !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该包含该图层
|
||||
* 检查图层是否应该包含在导出中
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @returns {Boolean} 是否包含
|
||||
* @returns {Boolean} 是否应该包含
|
||||
* @private
|
||||
*/
|
||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
|
||||
// 背景图层
|
||||
if (!layer) return false;
|
||||
|
||||
// 检查背景图层
|
||||
if (layer.isBackground) {
|
||||
return isContainBg;
|
||||
}
|
||||
|
||||
// 固定图层
|
||||
// 检查固定图层
|
||||
if (layer.isFixed) {
|
||||
return isContainFixed;
|
||||
}
|
||||
|
||||
// 普通图层始终包含
|
||||
// 普通图层总是包含
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算对象组的边界
|
||||
* @param {Array} objects 对象数组
|
||||
* @returns {Object} 边界信息 {left, top, width, height}
|
||||
* @private
|
||||
*/
|
||||
_calculateGroupBounds(objects) {
|
||||
if (!objects || objects.length === 0) {
|
||||
return { left: 0, top: 0, width: 100, height: 100 };
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
if (!obj) return;
|
||||
|
||||
try {
|
||||
const bounds = obj.getBoundingRect();
|
||||
minX = Math.min(minX, bounds.left);
|
||||
minY = Math.min(minY, bounds.top);
|
||||
maxX = Math.max(maxX, bounds.left + bounds.width);
|
||||
maxY = Math.max(maxY, bounds.top + bounds.height);
|
||||
} catch (error) {
|
||||
console.warn("计算对象边界失败:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有有效边界,使用默认值
|
||||
if (minX === Infinity || minY === Infinity) {
|
||||
return { left: 0, top: 0, width: 100, height: 100 };
|
||||
}
|
||||
|
||||
return {
|
||||
left: minX,
|
||||
top: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高质量导出画布
|
||||
* @param {Object} bounds 边界信息
|
||||
* @param {Number} scaleFactor 缩放因子
|
||||
* @returns {HTMLCanvasElement} 画布元素
|
||||
* @private
|
||||
*/
|
||||
_createHighQualityExportCanvas(bounds, scaleFactor) {
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// 设置画布的实际像素尺寸(用于高清导出)
|
||||
canvas.width = bounds.width * scaleFactor;
|
||||
canvas.height = bounds.height * scaleFactor;
|
||||
|
||||
// 设置画布的显示尺寸(CSS尺寸)
|
||||
canvas.style.width = bounds.width + "px";
|
||||
canvas.style.height = bounds.height + "px";
|
||||
|
||||
// 启用高质量渲染
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高质量临时Fabric画布
|
||||
* @param {HTMLCanvasElement} canvas 画布元素
|
||||
* @param {Object} bounds 边界信息
|
||||
* @param {Number} scaleFactor 缩放因子
|
||||
* @returns {fabric.StaticCanvas} Fabric画布
|
||||
* @private
|
||||
*/
|
||||
_createHighQualityTempFabricCanvas(canvas, bounds, scaleFactor) {
|
||||
const tempFabricCanvas = new fabric.StaticCanvas(canvas, {
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
backgroundColor: null, // 透明背景
|
||||
});
|
||||
|
||||
// 启用高清缩放和图像平滑
|
||||
tempFabricCanvas.enableRetinaScaling = true;
|
||||
tempFabricCanvas.imageSmoothingEnabled = true;
|
||||
tempFabricCanvas.setZoom(scaleFactor);
|
||||
|
||||
return tempFabricCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆对象并添加到临时画布(带偏移处理)
|
||||
* @param {fabric.StaticCanvas} tempCanvas 临时画布
|
||||
* @param {Object} obj 原始对象
|
||||
* @param {Object} bounds 边界信息
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {Promise<Object>} 克隆的对象
|
||||
* @private
|
||||
*/
|
||||
async _cloneAndAddObjectWithOffset(
|
||||
tempCanvas,
|
||||
obj,
|
||||
bounds,
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen
|
||||
) {
|
||||
if (!obj) return null;
|
||||
|
||||
try {
|
||||
const cloned = await this._cloneObjectForExport(
|
||||
obj,
|
||||
isRedGreenMode && restoreOpacityInRedGreen
|
||||
);
|
||||
|
||||
if (cloned) {
|
||||
// 调整对象位置,减去边界偏移量,使对象在新画布中正确定位
|
||||
cloned.set({
|
||||
left: cloned.left - bounds.left,
|
||||
top: cloned.top - bounds.top,
|
||||
});
|
||||
|
||||
// 更新对象坐标
|
||||
cloned.setCoords();
|
||||
|
||||
// 添加到临时画布
|
||||
tempCanvas.add(cloned);
|
||||
|
||||
return cloned;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("克隆并添加对象失败:", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象作为组导出(高质量版本)
|
||||
* @param {Array} objectsToExport 要导出的对象数组
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Number} scaleFactor 缩放因子,用于高清导出
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {Promise<String>} 图片数据URL
|
||||
* @private
|
||||
*/
|
||||
async _exportObjectsAsGroup(
|
||||
objectsToExport,
|
||||
expPicType,
|
||||
scaleFactor = 2,
|
||||
isRedGreenMode = false,
|
||||
restoreOpacityInRedGreen = true
|
||||
) {
|
||||
if (!objectsToExport || objectsToExport.length === 0) {
|
||||
throw new Error("没有可导出的对象");
|
||||
}
|
||||
|
||||
// 计算所有对象的边界
|
||||
const bounds = this._calculateGroupBounds(objectsToExport);
|
||||
console.log("导出边界:", bounds);
|
||||
|
||||
// 创建高质量临时画布
|
||||
const tempCanvas = this._createHighQualityExportCanvas(bounds, scaleFactor);
|
||||
const tempFabricCanvas = this._createHighQualityTempFabricCanvas(
|
||||
tempCanvas,
|
||||
bounds,
|
||||
scaleFactor
|
||||
);
|
||||
|
||||
try {
|
||||
// 克隆所有对象并添加到临时画布
|
||||
const clonedObjects = [];
|
||||
for (const obj of objectsToExport) {
|
||||
const cloned = await this._cloneAndAddObjectWithOffset(
|
||||
tempFabricCanvas,
|
||||
obj,
|
||||
bounds,
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen
|
||||
);
|
||||
if (cloned) {
|
||||
clonedObjects.push(cloned);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`成功克隆 ${clonedObjects.length} 个对象进行导出`);
|
||||
|
||||
// 渲染画布
|
||||
tempFabricCanvas.renderAll();
|
||||
|
||||
// 生成高质量数据URL
|
||||
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
||||
} finally {
|
||||
this._cleanupTempCanvas(tempFabricCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层下标为1的对象作为裁剪路径
|
||||
* @param {Number} layerIndex 图层下标
|
||||
* @param {Object} fixedBounds 固定图层边界
|
||||
* @returns {Promise<Object|null>} 裁剪路径对象
|
||||
* @private
|
||||
*/
|
||||
async _getLayerClipPathObject(layerIndex, fixedBounds) {
|
||||
try {
|
||||
const allLayers = this._getAllLayers();
|
||||
|
||||
// 获取指定下标的图层(从底到顶,下标0是最底层)
|
||||
const targetLayerIndex = layerIndex;
|
||||
|
||||
if (targetLayerIndex < 0 || targetLayerIndex >= allLayers.length) {
|
||||
console.warn(
|
||||
`图层下标 ${layerIndex} 超出范围,总图层数: ${allLayers.length}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetLayer = allLayers[targetLayerIndex];
|
||||
|
||||
if (
|
||||
!targetLayer ||
|
||||
!targetLayer.visible ||
|
||||
!targetLayer.fabricObjects ||
|
||||
targetLayer.fabricObjects.length === 0
|
||||
) {
|
||||
console.warn(`图层下标 ${layerIndex} 不可见或没有对象`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取图层中的第一个对象作为裁剪路径
|
||||
const clipObject = targetLayer.fabricObjects[0];
|
||||
|
||||
if (!clipObject) {
|
||||
console.warn(`图层下标 ${layerIndex} 中没有可用的裁剪对象`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 克隆对象作为裁剪路径
|
||||
const clonedClipPath = await this._cloneObjectForExport(
|
||||
clipObject,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if (!clonedClipPath) {
|
||||
console.warn(`无法克隆图层下标 ${layerIndex} 的裁剪对象`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 调整裁剪路径的位置相对于固定图层
|
||||
clonedClipPath.set({
|
||||
left: clonedClipPath.left - fixedBounds.left,
|
||||
top: clonedClipPath.top - fixedBounds.top,
|
||||
absolutePositioned: true, // 使用绝对定位
|
||||
});
|
||||
|
||||
// 更新坐标
|
||||
clonedClipPath.setCoords();
|
||||
|
||||
console.log(`成功创建图层下标 ${layerIndex} 的裁剪路径:`, {
|
||||
objectType: clonedClipPath.type,
|
||||
position: { left: clonedClipPath.left, top: clonedClipPath.top },
|
||||
size: { width: clonedClipPath.width, height: clonedClipPath.height },
|
||||
});
|
||||
|
||||
return clonedClipPath;
|
||||
} catch (error) {
|
||||
console.error(`获取图层下标 ${layerIndex} 裁剪路径失败:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,8 @@ import {
|
||||
BackgroundSizeCommand,
|
||||
BackgroundSizeWithScaleCommand,
|
||||
} from "../commands/BackgroundCommands";
|
||||
import {
|
||||
RasterizeLayerCommand,
|
||||
MergeGroupLayerCommand,
|
||||
} from "../commands/GroupCommands";
|
||||
import { MergeGroupLayerCommand } from "../commands/GroupCommands";
|
||||
import { RasterizeLayerCommand } from "../commands/RasterizeLayerCommand";
|
||||
|
||||
// 导入图层排序相关类和混入
|
||||
import {
|
||||
@@ -59,8 +57,13 @@ import {
|
||||
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
import { isBoolean, template } from "lodash-es";
|
||||
import { findObjectById, optimizeCanvasRendering } from "../utils/helper";
|
||||
import {
|
||||
findObjectById,
|
||||
generateId,
|
||||
optimizeCanvasRendering,
|
||||
} from "../utils/helper";
|
||||
import { message } from "ant-design-vue";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 图层管理器 - 负责管理画布上的所有图层
|
||||
@@ -168,17 +171,17 @@ export class LayerManager {
|
||||
* 根据当前编辑模式和图层状态设置对象的交互属性
|
||||
* @private
|
||||
*/
|
||||
updateLayersObjectsInteractivity(isUseOptimize = true) {
|
||||
async updateLayersObjectsInteractivity(isUseOptimize = true) {
|
||||
if (!this.canvas) return;
|
||||
if (isUseOptimize) {
|
||||
// 优化渲染 - 统一批处理 支持异步回调
|
||||
optimizeCanvasRendering(this.canvas, () => {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 应用图层交互规则
|
||||
this._applyInteractionRules();
|
||||
await this._applyInteractionRules();
|
||||
});
|
||||
} else {
|
||||
// 直接应用图层交互规则
|
||||
this._applyInteractionRules();
|
||||
await this._applyInteractionRules();
|
||||
}
|
||||
|
||||
// // 性能优化:使用requestAnimationFrame
|
||||
@@ -201,7 +204,7 @@ export class LayerManager {
|
||||
// this.canvas.renderAll(); // 确保画布重新渲染 - 同步渲染
|
||||
// });
|
||||
}
|
||||
_setObjectInteractivity(obj, layer, editorMode) {
|
||||
async _setObjectInteractivity(obj, layer, editorMode) {
|
||||
// 设置可见性
|
||||
obj.visible = layer.visible;
|
||||
|
||||
@@ -281,12 +284,14 @@ export class LayerManager {
|
||||
if (layer.blendMode) obj.globalCompositeOperation = layer.blendMode;
|
||||
if (layer.clippingMask) {
|
||||
const { object } = findObjectById(this.canvas, layer.clippingMask?.id);
|
||||
obj.clipPath = object || null;
|
||||
if (object) {
|
||||
obj.clipPath = object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 私有方法:应用交互规则
|
||||
_applyInteractionRules() {
|
||||
async _applyInteractionRules() {
|
||||
console.log("updateLayersObjectsInteractivity ===>", this.editorMode);
|
||||
const objects = this.canvas.getObjects();
|
||||
const editorMode = this.editorMode || CanvasConfig.defaultTool;
|
||||
@@ -302,7 +307,7 @@ export class LayerManager {
|
||||
});
|
||||
|
||||
// 批量更新对象
|
||||
objects.forEach((obj) => {
|
||||
objects.forEach(async (obj) => {
|
||||
const layer = layerMap[obj.layerId];
|
||||
|
||||
if (!obj.layerId) {
|
||||
@@ -316,15 +321,15 @@ export class LayerManager {
|
||||
if (!layer) return;
|
||||
|
||||
// 设置一级图层对象的交互性
|
||||
this._setObjectInteractivity(obj, layer, editorMode);
|
||||
await this._setObjectInteractivity(obj, layer, editorMode);
|
||||
|
||||
// 设置子图层对象的交互性
|
||||
layer?.childLayer?.forEach((childLayer) => {
|
||||
layer?.childLayer?.forEach(async (childLayer) => {
|
||||
const childObj = this.canvas
|
||||
.getObjects()
|
||||
.find((o) => o.layerId === childLayer.id);
|
||||
if (childObj) {
|
||||
this._setObjectInteractivity(childObj, childLayer, editorMode);
|
||||
await this._setObjectInteractivity(childObj, childLayer, editorMode);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -336,9 +341,13 @@ export class LayerManager {
|
||||
* @param {Object} options 额外选项
|
||||
* @returns {string} 新创建的图层ID
|
||||
*/
|
||||
createLayer(name = null, type = LayerType.EMPTY, options = {}) {
|
||||
async createLayer(name = null, type = LayerType.EMPTY, options = {}) {
|
||||
// 生成唯一ID
|
||||
const layerId = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
const layerId =
|
||||
options.id ||
|
||||
options.layerId ||
|
||||
generateId("layer_") ||
|
||||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
const layerIndex = this.layers.value.length;
|
||||
|
||||
// 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置
|
||||
@@ -380,15 +389,15 @@ export class LayerManager {
|
||||
command.undoable = options.undoable;
|
||||
|
||||
// 如果是第一个图层,或者普通图层数量小于等于3,设置为不可撤销
|
||||
if (this.layers.value.length === 3 || normalLayersCount <= 1) {
|
||||
if (this.layers.value.length === 3 || normalLayersCount < 1) {
|
||||
command.undoable = false;
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
if (this.commandManager) {
|
||||
this.commandManager.execute(command);
|
||||
await this.commandManager.execute(command);
|
||||
} else {
|
||||
command.execute();
|
||||
await command.execute();
|
||||
}
|
||||
|
||||
return layerId;
|
||||
@@ -505,7 +514,7 @@ export class LayerManager {
|
||||
/**
|
||||
* 初始化图层,确保有背景层、固定图层和一个空白图层
|
||||
*/
|
||||
initializeLayers() {
|
||||
async initializeLayers() {
|
||||
// 如果没有任何图层,创建背景层、固定图层和一个空白图层
|
||||
if (this.layers.value.length === 0) {
|
||||
// 创建背景图层
|
||||
@@ -515,7 +524,7 @@ export class LayerManager {
|
||||
this.createFixedLayer();
|
||||
|
||||
// 创建一个空白图层(默认位于背景图层和固定图层之上)
|
||||
this.createLayer("图层 1");
|
||||
await this.createLayer("图层 1");
|
||||
} else {
|
||||
// 检查是否已有背景层
|
||||
const hasBackgroundLayer = this.layers.value.some(
|
||||
@@ -539,7 +548,7 @@ export class LayerManager {
|
||||
);
|
||||
|
||||
if (!hasNormalLayer) {
|
||||
this.createLayer("图层 1");
|
||||
await this.createLayer("图层 1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,14 +688,26 @@ export class LayerManager {
|
||||
* @return {Object} layer 图层对象
|
||||
*/
|
||||
getActiveLayer() {
|
||||
const { layer: activeLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
this.activeLayerId.value
|
||||
);
|
||||
if (activeLayer) {
|
||||
return activeLayer;
|
||||
} else {
|
||||
console.warn("没有活动图层");
|
||||
if (!this.activeLayerId.value) {
|
||||
console.warn(
|
||||
"没有活动图层ID,无法获取活动图层 ==== 默认设置第一个图层为活动图层"
|
||||
);
|
||||
this.activeLayerId.value = this.layers.value[0]?.id || null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { layer: activeLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
this.activeLayerId.value
|
||||
);
|
||||
if (activeLayer) {
|
||||
return activeLayer;
|
||||
} else {
|
||||
console.warn("没有活动图层");
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取活动图层失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -868,27 +889,21 @@ export class LayerManager {
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {boolean} 更新后的可见性状态
|
||||
*/
|
||||
toggleLayerVisibility(layerId) {
|
||||
async toggleLayerVisibility(layerId) {
|
||||
// 直接创建和执行命令
|
||||
const command = new ToggleLayerVisibilityCommand({
|
||||
canvas: this.canvas,
|
||||
layers: this.layers,
|
||||
layerId: layerId,
|
||||
layerManager: this,
|
||||
});
|
||||
|
||||
// 执行命令
|
||||
if (this.commandManager) {
|
||||
this.commandManager.execute(command);
|
||||
return await this.commandManager.execute(command);
|
||||
} else {
|
||||
command.execute();
|
||||
return await command.execute();
|
||||
}
|
||||
|
||||
// 更新对象交互性
|
||||
this.updateLayersObjectsInteractivity();
|
||||
|
||||
// 获取当前可见性
|
||||
const layer = this.layers.value.find((layer) => layer.id === layerId);
|
||||
return layer ? layer.visible : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2029,7 +2044,7 @@ export class LayerManager {
|
||||
* @param {Object} options 文本选项
|
||||
* @returns {Object} 创建的文本对象
|
||||
*/
|
||||
createTextLayerWithObject(textObject, options = {}) {
|
||||
async createTextLayerWithObject(textObject, options = {}) {
|
||||
if (!this.canvas || !textObject) return null;
|
||||
|
||||
// 确保对象有ID
|
||||
@@ -2038,7 +2053,7 @@ export class LayerManager {
|
||||
|
||||
// 创建文本图层
|
||||
const layerName = options.name || "文本图层";
|
||||
const layerId = this.createLayer(layerName, LayerType.TEXT, {
|
||||
const layerId = await this.createLayer(layerName, LayerType.TEXT, {
|
||||
layerProperties: {
|
||||
text: options.text || textObject.text || "新文本",
|
||||
fontFamily: options.fontFamily || textObject.fontFamily || "Arial",
|
||||
@@ -2075,7 +2090,9 @@ export class LayerManager {
|
||||
const layer = this.getLayerById(layerId);
|
||||
if (layer) {
|
||||
layer.fabricObjects = layer.fabricObjects || [];
|
||||
layer.fabricObjects.push(textObject);
|
||||
layer.fabricObjects.push(
|
||||
textObject.toObject(["id", "layerId", "layerName"])
|
||||
);
|
||||
}
|
||||
|
||||
// 设置此图层为活动图层
|
||||
@@ -2218,7 +2235,7 @@ export class LayerManager {
|
||||
* @param {string} parentId 父图层ID
|
||||
* @returns {boolean} 是否删除成功
|
||||
*/
|
||||
removeChildLayer(layerId, parentId) {
|
||||
async removeChildLayer(layerId, parentId) {
|
||||
// 直接创建和执行命令
|
||||
const command = new RemoveChildLayerCommand({
|
||||
canvas: this.canvas,
|
||||
@@ -2231,9 +2248,9 @@ export class LayerManager {
|
||||
|
||||
// 执行命令
|
||||
if (this.commandManager) {
|
||||
return this.commandManager.execute(command);
|
||||
return await this.commandManager.execute(command);
|
||||
} else {
|
||||
return command.execute();
|
||||
return await command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2298,28 +2315,22 @@ export class LayerManager {
|
||||
* @param {string} parentId 父图层ID
|
||||
* @returns {boolean} 更新后的可见性状态
|
||||
*/
|
||||
toggleChildLayerVisibility(layerId, parentId) {
|
||||
async toggleChildLayerVisibility(layerId, parentId) {
|
||||
// 直接创建和执行命令
|
||||
const command = new ToggleChildLayerVisibilityCommand({
|
||||
canvas: this.canvas,
|
||||
layers: this.layers,
|
||||
layerId: layerId,
|
||||
parentId: parentId,
|
||||
layerManager: this,
|
||||
});
|
||||
|
||||
// 执行命令
|
||||
if (this.commandManager) {
|
||||
this.commandManager.execute(command);
|
||||
return await this.commandManager.execute(command);
|
||||
} else {
|
||||
command.execute();
|
||||
return await command.execute();
|
||||
}
|
||||
|
||||
// 更新对象交互性
|
||||
this.updateLayersObjectsInteractivity();
|
||||
|
||||
// 获取当前可见性
|
||||
const childLayer = this.findChildLayer(layerId, parentId);
|
||||
return childLayer ? childLayer.visible : false;
|
||||
}
|
||||
|
||||
// ==================== 红绿图模式相关操作 ====================
|
||||
@@ -2782,7 +2793,12 @@ export class LayerManager {
|
||||
}
|
||||
|
||||
// 查找目标图层
|
||||
const targetLayer = this.getLayerById(targetLayerId);
|
||||
// const targetLayer = this.getLayerById(targetLayerId);
|
||||
const { layer: targetLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
targetLayerId
|
||||
);
|
||||
|
||||
if (!targetLayer) {
|
||||
console.error($t("图层不存在", { layerId: targetLayerId }));
|
||||
return false;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { findObjectById } from "../utils/helper";
|
||||
import { findLayerRecursively } from "../utils/layerHelper";
|
||||
import { createRasterizedImage } from "../utils/rasterizedImage";
|
||||
|
||||
/**
|
||||
* 缩略图管理器 - 负责生成和缓存图层和元素的预览缩略图
|
||||
*/
|
||||
@@ -6,12 +10,8 @@ export class ThumbnailManager {
|
||||
this.canvas = canvas;
|
||||
this.layers = options.layers || []; // 图层管理器
|
||||
this.layerThumbSize = options.layerThumbSize || { width: 48, height: 48 };
|
||||
this.elementThumbSize = options.elementThumbSize || {
|
||||
width: 36,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
this.layerThumbnails = new Map(); // 图层缩略图缓存
|
||||
this.elementThumbnails = new Map(); // 元素缩略图缓存
|
||||
this.defaultThumbnail =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // 1x1 透明图
|
||||
}
|
||||
@@ -20,279 +20,31 @@ export class ThumbnailManager {
|
||||
* 生成图层缩略图
|
||||
* @param {Object} layer 图层对象ID
|
||||
*/
|
||||
generateLayerThumbnail(layerId) {
|
||||
// const layer = this?.layers.value?.find((layer) => layer.id === layerId);
|
||||
// if (!layer) return;
|
||||
// // 延迟执行,避免阻塞UI
|
||||
// requestAnimationFrame(() => {
|
||||
// this._generateLayerThumbnailNow(layer);
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即生成图层缩略图
|
||||
* @param {Object} layer 图层对象
|
||||
* @private
|
||||
*/
|
||||
_generateLayerThumbnailNow(layer) {
|
||||
if (
|
||||
!layer ||
|
||||
!this.canvas ||
|
||||
!layer.fabricObjects ||
|
||||
!layer.fabricObjects?.length
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
let thumbnail = null;
|
||||
|
||||
if (!layer.children?.length) {
|
||||
// 如果是元素图层,直接生成元素缩略图
|
||||
thumbnail = this._generateThumbnailFromObjects(
|
||||
layer.fabricObjects,
|
||||
this.layerThumbSize.width,
|
||||
this.layerThumbSize.height
|
||||
);
|
||||
} else if (layer.type === "group" || layer.children?.length) {
|
||||
const fabricObjects = layer.children.reduce((pre, next) => {
|
||||
if (next.fabricObjects.length) {
|
||||
pre.push(...next.fabricObjects);
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
// 如果是分组图层,合并所有子对象的缩略图
|
||||
thumbnail = this._generateThumbnailFromObjects(
|
||||
fabricObjects,
|
||||
this.layerThumbSize.width,
|
||||
this.layerThumbSize.height
|
||||
);
|
||||
}
|
||||
|
||||
// 保存到缩略图缓存
|
||||
if (thumbnail) {
|
||||
this.layerThumbnails.set(layer.id, thumbnail);
|
||||
} else {
|
||||
// 如果无法生成缩略图,使用默认缩略图
|
||||
this.layerThumbnails.set(layer.id, this.defaultThumbnail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成图层缩略图出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成元素缩略图
|
||||
* @param {Object} element 元素对象
|
||||
* @param {Object} fabricObject fabric对象
|
||||
*/
|
||||
generateElementThumbnail(element, fabricObject) {
|
||||
if (!element || !element.id || !fabricObject) return;
|
||||
async generateLayerThumbnail(layerId) {
|
||||
const fabricObjects = this._collectLayersAndObjects(layerId);
|
||||
|
||||
// 延迟执行,避免阻塞UI
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
const thumbnail = this._generateThumbnailFromObject(
|
||||
fabricObject,
|
||||
this.elementThumbSize.width,
|
||||
this.elementThumbSize.height
|
||||
);
|
||||
fabricObjects.length > 0 &&
|
||||
requestAnimationFrame(async () => {
|
||||
const base64 = await this._generateLayerThumbnailNow(fabricObjects);
|
||||
this.layerThumbnails.set(layerId, base64);
|
||||
try {
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId
|
||||
);
|
||||
if (layer) {
|
||||
layer.thumbnailUrl = base64; // 更新图层对象的缩略图
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
this.elementThumbnails.set(element.id, thumbnail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成元素缩略图出错:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从fabric对象生成缩略图
|
||||
* @param {Object} obj fabric对象
|
||||
* @param {Number} width 缩略图宽度
|
||||
* @param {Number} height 缩略图高度
|
||||
* @returns {String} 缩略图数据URL
|
||||
* @private
|
||||
*/
|
||||
_generateThumbnailFromObject(obj, width, height) {
|
||||
if (!obj || !this.canvas) return null;
|
||||
|
||||
// 保存对象状态
|
||||
const originalState = {
|
||||
active: obj.active,
|
||||
visible: obj.visible,
|
||||
left: obj.left,
|
||||
top: obj.top,
|
||||
scaleX: obj.scaleX,
|
||||
scaleY: obj.scaleY,
|
||||
opacity: obj.opacity,
|
||||
};
|
||||
|
||||
// 临时修改对象状态
|
||||
obj.set({
|
||||
active: false,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
// 创建临时画布
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = width;
|
||||
tempCanvas.height = height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 获取对象边界
|
||||
const bounds = obj.getBoundingRect();
|
||||
|
||||
// 绘制缩略图
|
||||
try {
|
||||
// 清空画布
|
||||
tempCtx.clearRect(0, 0, width, height);
|
||||
|
||||
// 计算缩放比例
|
||||
const scaleFactorX = width / bounds.width;
|
||||
const scaleFactorY = height / bounds.height;
|
||||
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
|
||||
|
||||
// 居中绘制
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.scale(scaleFactor, scaleFactor);
|
||||
tempCtx.translate(
|
||||
-bounds.left - bounds.width / 2,
|
||||
-bounds.top - bounds.height / 2
|
||||
);
|
||||
|
||||
// 绘制对象
|
||||
obj.render(tempCtx);
|
||||
|
||||
tempCtx.restore();
|
||||
|
||||
// 转换为数据URL
|
||||
const dataUrl = tempCanvas.toDataURL("image/png");
|
||||
|
||||
// 恢复对象状态
|
||||
obj.set(originalState);
|
||||
|
||||
return dataUrl;
|
||||
} catch (error) {
|
||||
console.error("绘制对象缩略图出错:", error);
|
||||
|
||||
// 恢复对象状态
|
||||
obj.set(originalState);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多个fabric对象生成组合缩略图
|
||||
* @param {Array} objects fabric对象数组
|
||||
* @param {Number} width 缩略图宽度
|
||||
* @param {Number} height 缩略图高度
|
||||
* @returns {String} 缩略图数据URL
|
||||
* @private
|
||||
*/
|
||||
_generateThumbnailFromObjects(objects, width, height) {
|
||||
if (!objects || !objects.length || !this.canvas) return null;
|
||||
|
||||
// 创建临时画布
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = width;
|
||||
tempCanvas.height = height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 计算所有对象的总边界
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
if (!obj.visible) return;
|
||||
|
||||
const bounds = obj.getBoundingRect();
|
||||
minX = Math.min(minX, bounds.left);
|
||||
minY = Math.min(minY, bounds.top);
|
||||
maxX = Math.max(maxX, bounds.left + bounds.width);
|
||||
maxY = Math.max(maxY, bounds.top + bounds.height);
|
||||
});
|
||||
|
||||
const groupWidth = maxX - minX;
|
||||
const groupHeight = maxY - minY;
|
||||
|
||||
// 如果没有有效对象,返回null
|
||||
if (groupWidth <= 0 || groupHeight <= 0) return null;
|
||||
|
||||
// 保存对象状态
|
||||
const originalStates = objects.map((obj) => ({
|
||||
obj,
|
||||
state: {
|
||||
active: obj.active,
|
||||
visible: obj.visible,
|
||||
opacity: obj.opacity,
|
||||
},
|
||||
}));
|
||||
|
||||
// 临时修改对象状态
|
||||
originalStates.forEach((item) => {
|
||||
item.obj.set({
|
||||
active: false,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
});
|
||||
});
|
||||
|
||||
// 绘制缩略图
|
||||
try {
|
||||
// 清空画布
|
||||
tempCtx.clearRect(0, 0, width, height);
|
||||
|
||||
// 计算缩放比例
|
||||
const scaleFactorX = width / groupWidth;
|
||||
const scaleFactorY = height / groupHeight;
|
||||
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
|
||||
|
||||
// 居中绘制
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.scale(scaleFactor, scaleFactor);
|
||||
tempCtx.translate(-(minX + groupWidth / 2), -(minY + groupHeight / 2));
|
||||
|
||||
// 按顺序绘制所有对象
|
||||
objects.forEach((obj) => {
|
||||
if (obj.visible) {
|
||||
obj.render(tempCtx);
|
||||
if (parent) {
|
||||
// 如果是组图层,则同步更新父图层的缩略图
|
||||
this.generateLayerThumbnail(parent.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成图层缩略图时出错:", error);
|
||||
}
|
||||
});
|
||||
|
||||
tempCtx.restore();
|
||||
|
||||
// 转换为数据URL
|
||||
const dataUrl = tempCanvas.toDataURL("image/png");
|
||||
|
||||
// 恢复对象状态
|
||||
originalStates.forEach((item) => {
|
||||
item.obj.set(item.state);
|
||||
});
|
||||
|
||||
return dataUrl;
|
||||
} catch (error) {
|
||||
console.error("绘制组合缩略图出错:", error);
|
||||
|
||||
// 恢复对象状态
|
||||
originalStates.forEach((item) => {
|
||||
item.obj.set(item.state);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,13 +55,133 @@ export class ThumbnailManager {
|
||||
if (!layers || !Array.isArray(layers)) return;
|
||||
|
||||
// 使用requestAnimationFrame批量生成,避免阻塞主线程
|
||||
requestAnimationFrame(() => {
|
||||
layers.forEach((layer) => {
|
||||
if (layer && layer.id) {
|
||||
this._generateLayerThumbnailNow(layer);
|
||||
layers.forEach((layer) => {
|
||||
if (layer && layer.id) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
if (layer.children && layer.children.length) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
async _generateLayerThumbnailNow(fabricObjects) {
|
||||
if (!fabricObjects || fabricObjects.length === 0) {
|
||||
console.warn("⚠️ 没有对象需要生成缩略图,返回默认缩略图");
|
||||
return this.defaultThumbnail;
|
||||
}
|
||||
return await createRasterizedImage({
|
||||
canvas: this.canvas, // 画布对象 必填
|
||||
fabricObjects, // 要栅格化的对象列表 - 按顺序 必填
|
||||
// maskObject = null, // 用于裁剪的对象 - 可选 // TODO: 后期看是否需要裁剪
|
||||
trimWhitespace: true, // 是否裁剪空白区域
|
||||
trimPadding: 2, // 裁剪边距
|
||||
quality: 0.8, // 图像质量
|
||||
format: "png", // 图像格式
|
||||
scaleFactor: 1, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL: true, // 是否返回DataURL而不是fabric.Image对象
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集要栅格化的图层和对象
|
||||
* @private
|
||||
*/
|
||||
_collectLayersAndObjects(layerId) {
|
||||
if (!layerId) {
|
||||
console.warn("⚠️ 无效的图层ID,无法收集对象");
|
||||
return [];
|
||||
}
|
||||
|
||||
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
||||
let layersToRasterize = [];
|
||||
|
||||
if (layer.children && layer.children.length > 0) {
|
||||
// 组图层:收集自身和所有子图层
|
||||
layersToRasterize = this._collectLayersToRasterize(layer);
|
||||
} else {
|
||||
// 普通图层:只收集自身
|
||||
layersToRasterize = [layer];
|
||||
}
|
||||
|
||||
// 收集所有图层的fabricObjects并按画布z-index顺序排序
|
||||
const allCanvasObjects = this.canvas.getObjects();
|
||||
const objectsWithZIndex = [];
|
||||
|
||||
layersToRasterize.forEach((layer) => {
|
||||
if (layer.fabricObject) {
|
||||
// 如果图层本身有fabricObject,直接添加
|
||||
const { object } = findObjectById(this.canvas, layer.fabricObject.id);
|
||||
|
||||
if (object) {
|
||||
const zIndex = allCanvasObjects.indexOf(object);
|
||||
objectsWithZIndex.push({
|
||||
object: object,
|
||||
zIndex: zIndex,
|
||||
layerObj: layer.fabricObject,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 提取排序后的对象
|
||||
const objectsToRasterize = objectsWithZIndex.map((item) => item.object);
|
||||
|
||||
console.log(
|
||||
`📊 收集到 ${layersToRasterize.length} 个图层,${objectsToRasterize.length} 个对象进行栅格化`
|
||||
);
|
||||
console.log(
|
||||
"🔢 对象z-index顺序:",
|
||||
objectsWithZIndex.map((item) => ({
|
||||
id: item.object.id,
|
||||
type: item.object.type,
|
||||
zIndex: item.zIndex,
|
||||
}))
|
||||
);
|
||||
|
||||
return objectsToRasterize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集要栅格化的图层(递归收集子图层)
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,16 +194,6 @@ export class ThumbnailManager {
|
||||
return this.layerThumbnails.get(layerId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素缩略图
|
||||
* @param {String} elementId 元素ID
|
||||
* @returns {String|null} 缩略图URL或null
|
||||
*/
|
||||
getElementThumbnail(elementId) {
|
||||
if (!elementId) return null;
|
||||
return this.elementThumbnails.get(elementId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除图层缩略图
|
||||
* @param {String} layerId 图层ID
|
||||
@@ -342,22 +204,11 @@ export class ThumbnailManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除元素缩略图
|
||||
* @param {String} elementId 元素ID
|
||||
*/
|
||||
clearElementThumbnail(elementId) {
|
||||
if (elementId && this.elementThumbnails.has(elementId)) {
|
||||
this.elementThumbnails.delete(elementId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缩略图
|
||||
*/
|
||||
clearAllThumbnails() {
|
||||
this.layerThumbnails.clear();
|
||||
this.elementThumbnails.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { BrushStore } from "../store/BrushStore";
|
||||
import { BrushManager } from "./brushes/brushManager";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import { CreateTextCommand } from "../commands/TextCommands";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { InitLiquifyToolCommand } from "../commands/LiquifyCommands";
|
||||
import { RasterizeLayerCommand } from "../commands/GroupCommands";
|
||||
import { RasterizeLayerCommand } from "../commands/RasterizeLayerCommand";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { h } from "vue";
|
||||
|
||||
@@ -958,9 +959,33 @@ export class ToolManager {
|
||||
* @param {Number} y 文本位置y坐标
|
||||
* @param {Object} options 文本选项
|
||||
*/
|
||||
createText(x, y, options = {}) {
|
||||
async createText(x, y, options = {}) {
|
||||
// 使用命令模式创建文本
|
||||
if (!this.canvas || !this.layerManager) return null;
|
||||
if (this.commandManager) {
|
||||
const command = new CreateTextCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
x,
|
||||
y,
|
||||
textOptions: options,
|
||||
});
|
||||
// 执行命令
|
||||
return await this.commandManager.execute(command);
|
||||
} else {
|
||||
// 如果没有命令管理器,直接调用原有方法(兼容性)
|
||||
return await this._createTextDirect(x, y, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接创建文本的方法(用于向后兼容)
|
||||
* @param {Number} x 文本位置x坐标
|
||||
* @param {Number} y 文本位置y坐标
|
||||
* @param {Object} options 文本选项
|
||||
* @private
|
||||
*/
|
||||
_createTextDirect(x, y, options = {}) {
|
||||
// 默认文本属性
|
||||
const defaultOptions = {
|
||||
text: "双击编辑文本",
|
||||
@@ -992,7 +1017,6 @@ export class ToolManager {
|
||||
|
||||
// 创建文本图层并通过LayerManager添加到画布
|
||||
this.layerManager.createTextLayerWithObject(textObj, textOptions);
|
||||
this.canvas.renderAll();
|
||||
|
||||
return textObj;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# fabric-with-erasing 库的 erasable 属性功能使用指南
|
||||
|
||||
## 库功能概述
|
||||
|
||||
`fabric-with-erasing` 库提供了强大的基于属性的擦除控制功能,无需手动实现复杂的图层检查逻辑。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. erasable 属性的三种模式
|
||||
|
||||
- **`true`** (默认): 对象可以被擦除
|
||||
- **`false`**: 对象不能被擦除
|
||||
- **`'deep'`**: 对于组合对象,可以对内部可擦除的子对象进行细粒度控制
|
||||
|
||||
### 2. 选择性擦除机制
|
||||
|
||||
库内置了选择性擦除机制:
|
||||
- 橡皮擦会自动检测对象的 `erasable` 属性
|
||||
- 只有 `erasable !== false` 的对象才会被擦除
|
||||
- 支持复杂的嵌套对象结构
|
||||
|
||||
### 3. 反向擦除功能
|
||||
|
||||
- 设置 `brush.inverted = true` 可以实现"撤销擦除"效果
|
||||
- 恢复已被擦除的内容
|
||||
|
||||
## 项目中的优化实现
|
||||
|
||||
### LayerManager 优化
|
||||
|
||||
```javascript
|
||||
// 基于图层状态自动设置 erasable 属性
|
||||
obj.erasable = isInActiveLayer && layer.visible && !layer.locked && !layer.isBackground;
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- 只有活动图层、可见、非锁定、非背景的对象才可擦除
|
||||
- 自动处理复杂的权限逻辑
|
||||
- 性能优秀,无需手动遍历检查
|
||||
|
||||
### BrushManager 简化
|
||||
|
||||
```javascript
|
||||
// 直接使用库的 EraserBrush
|
||||
this.brush = new fabric.EraserBrush(this.canvas);
|
||||
this.brush.inverted = this.options.inverted || false;
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- 移除了复杂的手动图层检查逻辑
|
||||
- 直接利用库的内置功能
|
||||
- 支持反向擦除(恢复功能)
|
||||
- 代码更简洁、更可靠
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```javascript
|
||||
// 设置对象不可擦除
|
||||
fabricObject.erasable = false;
|
||||
|
||||
// 设置对象可擦除
|
||||
fabricObject.erasable = true;
|
||||
|
||||
// 组合对象的深度擦除控制
|
||||
group.erasable = 'deep';
|
||||
```
|
||||
|
||||
### 高级用法
|
||||
|
||||
```javascript
|
||||
// 启用反向擦除模式
|
||||
eraserBrush.inverted = true;
|
||||
|
||||
// 监听擦除事件
|
||||
canvas.on('erasing:start', () => {
|
||||
console.log('开始擦除');
|
||||
});
|
||||
|
||||
canvas.on('erasing:end', (e) => {
|
||||
console.log('擦除完成', e.targets);
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优势
|
||||
|
||||
1. **内置优化**: 库已经进行了性能优化,避免重复计算
|
||||
2. **事件驱动**: 基于事件的架构,响应更快
|
||||
3. **选择性渲染**: 只重新渲染需要更新的部分
|
||||
4. **内存效率**: 合理的对象管理和清理机制
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
- 完全兼容标准的 fabric.js API
|
||||
- 新增的 `erasable` 属性不会影响现有功能
|
||||
- 可以逐步迁移现有代码
|
||||
|
||||
## 建议
|
||||
|
||||
1. **简化现有实现**: 移除手动的图层检查逻辑,直接使用 `erasable` 属性
|
||||
2. **利用内置事件**: 使用库提供的擦除事件进行状态管理
|
||||
3. **测试反向擦除**: 尝试使用 `inverted` 属性实现撤销功能
|
||||
4. **性能测试**: 在大量对象的场景下测试性能表现
|
||||
|
||||
通过这些优化,你的项目可以获得更好的性能和更简洁的代码结构。
|
||||
280
src/component/Canvas/CanvasEditor/managers/brushes/README.md
Normal file
280
src/component/Canvas/CanvasEditor/managers/brushes/README.md
Normal file
@@ -0,0 +1,280 @@
|
||||
<!-- https://github.com/tennisonchan/fabric-brush?tab=readme-ov-file -->
|
||||
<!-- eraser_brush:https://unpkg.com/fabric@5.5.2/src/mixins/eraser_brush.mixin.js -->
|
||||
# 笔刷系统使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
这是一个基于插件架构的笔刷系统,允许轻松扩展和添加新的笔刷类型。整个系统由以下几个关键部分组成:
|
||||
|
||||
1. `BaseBrush` - 所有笔刷的基类
|
||||
2. `BrushRegistry` - 笔刷注册表,用于管理所有笔刷
|
||||
3. `BrushManager` - 笔刷管理器,处理笔刷的实例化和切换
|
||||
4. `BrushStore` - 笔刷状态存储
|
||||
|
||||
## 如何添加新笔刷
|
||||
|
||||
添加新笔刷只需简单几步:
|
||||
|
||||
### 1. 创建新的笔刷类
|
||||
|
||||
最简单的方法是继承 `BaseBrush` 类。在 `types` 目录下创建你的笔刷文件:
|
||||
|
||||
```javascript
|
||||
import { BaseBrush } from '../BaseBrush';
|
||||
|
||||
/**
|
||||
* 我的自定义笔刷
|
||||
*/
|
||||
export class MyCustomBrush extends BaseBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: 'my-custom-brush',
|
||||
name: '我的笔刷',
|
||||
description: '这是我自定义的笔刷',
|
||||
category: '自定义笔刷',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// 创建笔刷实例
|
||||
create() {
|
||||
// 创建底层fabric.js笔刷
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
// 配置笔刷
|
||||
configure(brush, options = {}) {
|
||||
// 设置基本属性
|
||||
if (options.color) brush.color = options.color;
|
||||
if (options.width !== undefined) brush.width = options.width;
|
||||
if (options.opacity !== undefined) brush.opacity = options.opacity;
|
||||
|
||||
// 设置自定义属性
|
||||
brush.strokeLineCap = 'round';
|
||||
brush.strokeLineJoin = 'round';
|
||||
// ...更多自定义设置
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册笔刷
|
||||
|
||||
有两种方式注册笔刷:
|
||||
|
||||
#### 方式一:使用BrushRegistry直接注册
|
||||
|
||||
```javascript
|
||||
import { brushRegistry } from '../BrushRegistry';
|
||||
import { MyCustomBrush } from './MyCustomBrush';
|
||||
|
||||
// 注册笔刷
|
||||
brushRegistry.register('my-custom-brush', MyCustomBrush, {
|
||||
name: '我的笔刷',
|
||||
description: '这是我自定义的笔刷',
|
||||
category: '自定义笔刷'
|
||||
});
|
||||
```
|
||||
|
||||
#### 方式二:通过BrushManager注册
|
||||
|
||||
```javascript
|
||||
import { BrushManager } from '../brushManager';
|
||||
import { MyCustomBrush } from './MyCustomBrush';
|
||||
|
||||
// 获取BrushManager实例
|
||||
const brushManager = new BrushManager({ canvas });
|
||||
|
||||
// 注册笔刷
|
||||
brushManager.registerBrush('my-custom-brush', MyCustomBrush, {
|
||||
name: '我的笔刷',
|
||||
description: '这是我自定义的笔刷',
|
||||
category: '自定义笔刷'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 使用笔刷
|
||||
|
||||
注册笔刷后,可以在应用中使用它:
|
||||
|
||||
```javascript
|
||||
// 切换到你的自定义笔刷
|
||||
brushManager.setBrushType('my-custom-brush');
|
||||
```
|
||||
|
||||
## 笔刷生命周期
|
||||
|
||||
每个笔刷有以下生命周期方法:
|
||||
|
||||
1. `constructor` - 创建笔刷类实例
|
||||
2. `create` - 创建底层fabric.js笔刷实例
|
||||
3. `configure` - 配置笔刷属性
|
||||
4. `onSelected` - 笔刷被选中时调用
|
||||
5. `onDeselected` - 笔刷被取消选中时调用
|
||||
6. `destroy` - 销毁笔刷实例释放资源
|
||||
|
||||
## 示例:创建具有独特行为的笔刷
|
||||
|
||||
这个例子创建了一个"脉冲笔刷",线条宽度会自动脉动变化:
|
||||
|
||||
```javascript
|
||||
import { BaseBrush } from '../BaseBrush';
|
||||
|
||||
export class PulseBrush extends BaseBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: 'pulse',
|
||||
name: '脉动笔刷',
|
||||
description: '线条宽度会自动脉动变化',
|
||||
category: '特效笔刷',
|
||||
...options
|
||||
});
|
||||
|
||||
this.originalWidth = options.width || 5;
|
||||
this.pulseRate = options.pulseRate || 0.1;
|
||||
this.pulseAmount = options.pulseAmount || 3;
|
||||
this.pulseTimer = null;
|
||||
}
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
// 覆盖鼠标按下方法,开始脉冲效果
|
||||
const originalMouseDown = this.brush.onMouseDown;
|
||||
this.brush.onMouseDown = (pointer, options) => {
|
||||
this.startPulse();
|
||||
return originalMouseDown.call(this.brush, pointer, options);
|
||||
};
|
||||
|
||||
// 覆盖鼠标松开方法,停止脉冲效果
|
||||
const originalMouseUp = this.brush.onMouseUp;
|
||||
this.brush.onMouseUp = (options) => {
|
||||
this.stopPulse();
|
||||
return originalMouseUp.call(this.brush, options);
|
||||
};
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
configure(brush, options = {}) {
|
||||
if (options.width !== undefined) {
|
||||
this.originalWidth = options.width;
|
||||
brush.width = this.originalWidth;
|
||||
}
|
||||
|
||||
if (options.color) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
if (options.pulseRate !== undefined) {
|
||||
this.pulseRate = options.pulseRate;
|
||||
}
|
||||
|
||||
if (options.pulseAmount !== undefined) {
|
||||
this.pulseAmount = options.pulseAmount;
|
||||
}
|
||||
}
|
||||
|
||||
startPulse() {
|
||||
this.stopPulse();
|
||||
|
||||
let phase = 0;
|
||||
this.pulseTimer = setInterval(() => {
|
||||
phase += this.pulseRate;
|
||||
const pulseFactor = Math.sin(phase) * this.pulseAmount;
|
||||
|
||||
if (this.brush) {
|
||||
this.brush.width = Math.max(1, this.originalWidth + pulseFactor);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
stopPulse() {
|
||||
if (this.pulseTimer) {
|
||||
clearInterval(this.pulseTimer);
|
||||
this.pulseTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onDeselected() {
|
||||
this.stopPulse();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopPulse();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用预设笔刷类型
|
||||
|
||||
系统内置了几种笔刷类型,可以直接使用:
|
||||
|
||||
- `pencil` - 基础铅笔笔刷
|
||||
- `spray` - 喷枪笔刷
|
||||
- `marker` - 马克笔笔刷
|
||||
- `eraser` - 橡皮擦笔刷
|
||||
- `texture` - 材质笔刷
|
||||
- `watercolor` - 水彩笔刷
|
||||
- `chalk` - 粉笔笔刷
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 1. 使用分类组织笔刷
|
||||
|
||||
注册笔刷时可以指定类别,便于在UI中分组展示:
|
||||
|
||||
```javascript
|
||||
brushRegistry.register('my-brush', MyBrushClass, {
|
||||
category: '艺术笔刷'
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 自定义笔刷参数
|
||||
|
||||
可以为笔刷添加特殊参数,例如:
|
||||
|
||||
```javascript
|
||||
// 笔刷类中
|
||||
setGlowIntensity(intensity) {
|
||||
this.glowIntensity = intensity;
|
||||
// 更新笔刷效果
|
||||
}
|
||||
|
||||
// 使用时
|
||||
const neonBrush = brushManager.setBrushType('neon');
|
||||
if (neonBrush && typeof neonBrush.setGlowIntensity === 'function') {
|
||||
neonBrush.setGlowIntensity(15);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何创建复杂的自定义笔刷效果?
|
||||
|
||||
对于复杂的效果,可以:
|
||||
|
||||
1. 覆盖fabric.js笔刷的关键方法,如`onMouseMove`
|
||||
2. 使用自定义渲染器处理绘制
|
||||
3. 结合Canvas API创建特殊效果
|
||||
|
||||
### 如何获取所有注册的笔刷?
|
||||
|
||||
```javascript
|
||||
// 获取所有笔刷
|
||||
const allBrushes = brushRegistry.getAllBrushes();
|
||||
|
||||
// 获取所有分类
|
||||
const categories = brushRegistry.getCategories();
|
||||
|
||||
// 获取指定分类的笔刷
|
||||
const artisticBrushes = brushRegistry.getBrushesByCategory('艺术笔刷');
|
||||
```
|
||||
@@ -76,7 +76,7 @@ export class BrushManager {
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("fur", FurBrush, {
|
||||
name: "Texture",
|
||||
name: "Fur",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
@@ -12,12 +12,11 @@
|
||||
* - https://mrdoob.com/projects/harmony/
|
||||
* - http://perfectionkills.com/exploring-canvas-drawing-techniques/
|
||||
*/
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
|
||||
(function (fabric) {
|
||||
/**
|
||||
* Trim a canvas. Returns the lezft-top coordinate where trimming began.
|
||||
* Trim a canvas. Returns the left-top coordinate where trimming began.
|
||||
* @param {canvas} canvas A canvas element to trim. This element will be trimmed (reference).
|
||||
* @returns {Object} Left-top coordinate of trimmed area. Example: {x:65, y:104}
|
||||
* @see: https://stackoverflow.com/a/22267731/3360038
|
||||
@@ -1744,5 +1743,4 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
|
||||
_render: function () {},
|
||||
}); // End WebBrush
|
||||
})(fabric);
|
||||
// })(typeof fabric !== "undefined" ? fabric : require("fabric").fabric);
|
||||
})(typeof fabric !== "undefined" ? fabric : require("fabric").fabric);
|
||||
|
||||
@@ -398,10 +398,7 @@ export class CanvasEventManager {
|
||||
// 延迟更新以确保对象完全添加
|
||||
setTimeout(() => {
|
||||
// 现在图层就是元素本身,直接更新元素的缩略图
|
||||
this.thumbnailManager.generateLayerThumbnail(
|
||||
e.target.layerId,
|
||||
e.target
|
||||
);
|
||||
this.thumbnailManager.generateLayerThumbnail(e.target.layerId);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
@@ -469,8 +466,6 @@ export class CanvasEventManager {
|
||||
this.canvas.on("object:removed", (e) => {
|
||||
if (this.thumbnailManager && e.target) {
|
||||
if (e.target.id) {
|
||||
this.thumbnailManager.clearElementThumbnail(e.target.id);
|
||||
|
||||
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
|
||||
if (e.target.parentId) {
|
||||
setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50);
|
||||
@@ -670,12 +665,6 @@ export class CanvasEventManager {
|
||||
// 生成图层缩略图
|
||||
this.thumbnailManager.generateLayerThumbnail(layer);
|
||||
}
|
||||
|
||||
// 同时也维护元素缩略图,以保持向后兼容性
|
||||
this.thumbnailManager.generateElementThumbnail(
|
||||
{ id: elementId, type: fabricObject.type },
|
||||
fabricObject
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
# CommandManager 命令管理器使用指南
|
||||
|
||||
## 📖 概述
|
||||
|
||||
CommandManager 是一个基于经典命令模式的撤销/重做系统,提供队列机制防止快速执行时命令丢失,支持复合命令进行批量操作,确保了高性能和稳定性。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
- **撤销/重做**: 基于经典命令模式的撤销重做功能
|
||||
- **队列机制**: 防止快速执行多个命令时的命令丢失
|
||||
- **串行化执行**: 确保命令按顺序执行,避免并发问题
|
||||
- **Promise支持**: 完全支持异步命令
|
||||
- **性能监控**: 可选的性能统计功能
|
||||
- **复合命令**: 支持批量操作和命令组合
|
||||
- **状态管理**: 实时状态监控和回调
|
||||
|
||||
## 🎯 复合命令详解
|
||||
|
||||
### 什么是复合命令?
|
||||
|
||||
复合命令是一组相关操作的集合,这些操作作为一个整体执行,支持命令间的值传递和条件执行。在Canvas编辑器中,复合命令特别适用于:
|
||||
|
||||
- 创建复杂组件(同时创建多个对象和图层)
|
||||
- 批量修改属性
|
||||
- 导入文件(可能涉及多个图层和对象的创建)
|
||||
- 复制粘贴操作
|
||||
|
||||
### 复合命令的执行流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[创建复合命令] --> B[添加子命令]
|
||||
B --> C[执行复合命令]
|
||||
C --> D[串行执行子命令]
|
||||
D --> E{执行成功?}
|
||||
E -->|是| F[记录已执行命令]
|
||||
F --> G[继续下一个命令]
|
||||
G --> D
|
||||
E -->|否| H[回滚已执行命令]
|
||||
H --> I[抛出错误]
|
||||
|
||||
C --> J[撤销复合命令]
|
||||
J --> K[逆序撤销子命令]
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 基础批量操作
|
||||
|
||||
```javascript
|
||||
import { CommandManager } from './CommandManager.js';
|
||||
import { Command } from '../../commands/Command.js';
|
||||
|
||||
// 创建命令管理器
|
||||
const commandManager = new CommandManager({
|
||||
maxHistorySize: 50,
|
||||
performanceManager: null
|
||||
});
|
||||
|
||||
// 创建简单命令
|
||||
class SetPropertyCommand extends Command {
|
||||
constructor(target, property, newValue) {
|
||||
super({ name: '设置属性', description: `设置${property}为${newValue}` });
|
||||
this.target = target;
|
||||
this.property = property;
|
||||
this.newValue = newValue;
|
||||
this.oldValue = null;
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.oldValue = this.target[this.property];
|
||||
this.target[this.property] = this.newValue;
|
||||
return { property: this.property, oldValue: this.oldValue, newValue: this.newValue };
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.target[this.property] = this.oldValue;
|
||||
return { property: this.property, restoredValue: this.oldValue };
|
||||
}
|
||||
}
|
||||
|
||||
// 使用批量操作
|
||||
const obj = { x: 0, y: 0, color: 'black' };
|
||||
|
||||
// 方法1: 使用 executeBatch
|
||||
await commandManager.executeBatch([
|
||||
new SetPropertyCommand(obj, 'x', 100),
|
||||
new SetPropertyCommand(obj, 'y', 200),
|
||||
new SetPropertyCommand(obj, 'color', 'red')
|
||||
], '批量设置对象属性');
|
||||
|
||||
console.log(obj); // { x: 100, y: 200, color: 'red' }
|
||||
|
||||
// 撤销时,所有属性会一次性恢复
|
||||
await commandManager.undo();
|
||||
console.log(obj); // { x: 0, y: 0, color: 'black' }
|
||||
```
|
||||
|
||||
### 2. 创建自定义复合命令
|
||||
|
||||
```javascript
|
||||
import { CompositeCommand } from '../../commands/Command.js';
|
||||
|
||||
// 支持值传递的复合命令
|
||||
class CreateLayerWithObjectsCommand extends CompositeCommand {
|
||||
constructor(layerName, objects) {
|
||||
super([], { name: '创建图层并添加对象' });
|
||||
this.layerName = layerName;
|
||||
this.objects = objects;
|
||||
this.layerId = null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
// 先创建图层
|
||||
const createLayerCmd = new CreateLayerCommand(this.layerName);
|
||||
this.layerId = await createLayerCmd.execute();
|
||||
this.addCommand(createLayerCmd);
|
||||
|
||||
// 使用返回的 layerId 添加对象
|
||||
for (const obj of this.objects) {
|
||||
const addObjectCmd = new AddObjectCommand(obj, this.layerId);
|
||||
await addObjectCmd.execute();
|
||||
this.addCommand(addObjectCmd);
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { layerId: this.layerId, objectCount: this.objects.length };
|
||||
}
|
||||
}
|
||||
|
||||
// 使用自定义复合命令
|
||||
const objects = [object1, object2, object3];
|
||||
const command = new CreateLayerWithObjectsCommand('新图层', objects);
|
||||
const result = await commandManager.execute(command);
|
||||
console.log(`创建了图层 ${result.layerId},包含 ${result.objectCount} 个对象`);
|
||||
```
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 1. 条件执行的复合命令
|
||||
|
||||
```javascript
|
||||
class ConditionalSetupCommand extends CompositeCommand {
|
||||
constructor(config) {
|
||||
super([], { name: '条件设置' });
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
// 总是创建基础对象
|
||||
const createCmd = new CreateObjectCommand(this.config.object);
|
||||
await createCmd.execute();
|
||||
this.addCommand(createCmd);
|
||||
|
||||
// 根据条件决定是否创建图层
|
||||
if (this.config.shouldCreateLayer) {
|
||||
const layerCmd = new CreateLayerCommand(this.config.layerName);
|
||||
await layerCmd.execute();
|
||||
this.addCommand(layerCmd);
|
||||
}
|
||||
|
||||
// 根据条件应用样式
|
||||
if (this.config.applyStyle) {
|
||||
const styleCmd = new ApplyStyleCommand(this.config.style);
|
||||
await styleCmd.execute();
|
||||
this.addCommand(styleCmd);
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { commandCount: this.commands.length };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 错误处理和回滚
|
||||
|
||||
```javascript
|
||||
class RobustBatchCommand extends CompositeCommand {
|
||||
constructor(operations) {
|
||||
super([], { name: '健壮的批量操作' });
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
for (const operation of this.operations) {
|
||||
const command = new operation.CommandClass(...operation.args);
|
||||
|
||||
// 执行命令
|
||||
await command.execute();
|
||||
this.addCommand(command);
|
||||
|
||||
console.log(`✅ 操作成功: ${command.constructor.name}`);
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { success: true, executedCount: this.commands.length };
|
||||
} catch (error) {
|
||||
console.error('❌ 批量操作失败:', error);
|
||||
|
||||
// 自动回滚已执行的命令
|
||||
await this._rollbackExecutedCommands();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 状态监控
|
||||
|
||||
```javascript
|
||||
// 设置状态变化回调
|
||||
commandManager.setChangeCallback((state) => {
|
||||
console.log('命令管理器状态:', {
|
||||
canUndo: state.canUndo,
|
||||
canRedo: state.canRedo,
|
||||
undoCount: state.undoCount,
|
||||
redoCount: state.redoCount,
|
||||
isExecuting: state.isExecuting,
|
||||
lastCommand: state.lastCommand
|
||||
});
|
||||
});
|
||||
|
||||
// 获取命令历史
|
||||
const history = commandManager.getHistory();
|
||||
console.log('撤销历史:', history.undoHistory);
|
||||
console.log('重做历史:', history.redoHistory);
|
||||
```
|
||||
|
||||
## 💡 实际应用场景
|
||||
|
||||
### 1. 复杂UI组件创建
|
||||
|
||||
```javascript
|
||||
class CreateComplexCardCommand extends CompositeCommand {
|
||||
constructor(canvas, config) {
|
||||
super([], { name: '创建复杂卡片' });
|
||||
this.canvas = canvas;
|
||||
this.config = config;
|
||||
this.createdObjects = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
// 创建背景
|
||||
const background = await this._createBackground();
|
||||
const bgCmd = new AddObjectCommand(background);
|
||||
await bgCmd.execute();
|
||||
this.addCommand(bgCmd);
|
||||
|
||||
// 创建头部
|
||||
const header = await this._createHeader();
|
||||
const headerCmd = new AddObjectCommand(header);
|
||||
await headerCmd.execute();
|
||||
this.addCommand(headerCmd);
|
||||
|
||||
// 创建内容区域
|
||||
const content = await this._createContent();
|
||||
const contentCmd = new AddObjectCommand(content);
|
||||
await contentCmd.execute();
|
||||
this.addCommand(contentCmd);
|
||||
|
||||
// 创建按钮
|
||||
const buttons = await this._createButtons();
|
||||
for (const button of buttons) {
|
||||
const buttonCmd = new AddObjectCommand(button);
|
||||
await buttonCmd.execute();
|
||||
this.addCommand(buttonCmd);
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { success: true, objectCount: this.commands.length };
|
||||
} catch (error) {
|
||||
await this._rollbackExecutedCommands();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _createBackground() {
|
||||
// 创建背景逻辑
|
||||
return new fabric.Rect({
|
||||
width: this.config.width,
|
||||
height: this.config.height,
|
||||
fill: this.config.backgroundColor
|
||||
});
|
||||
}
|
||||
|
||||
// ...existing code...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 批量导入处理
|
||||
|
||||
```javascript
|
||||
class ImportMultipleImagesCommand extends CompositeCommand {
|
||||
constructor(files, layerManager) {
|
||||
super([], { name: `导入${files.length}个图片文件` });
|
||||
this.files = files;
|
||||
this.layerManager = layerManager;
|
||||
this.importedLayers = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
for (const file of this.files) {
|
||||
// 加载图片
|
||||
const image = await this._loadImageFromFile(file);
|
||||
|
||||
// 创建图层
|
||||
const createLayerCmd = new CreateLayerCommand(file.name);
|
||||
const layerId = await createLayerCmd.execute();
|
||||
this.addCommand(createLayerCmd);
|
||||
|
||||
// 添加对象到图层
|
||||
const addObjectCmd = new AddObjectToLayerCommand(image, layerId);
|
||||
await addObjectCmd.execute();
|
||||
this.addCommand(addObjectCmd);
|
||||
|
||||
this.importedLayers.push(layerId);
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { importedLayers: this.importedLayers };
|
||||
} catch (error) {
|
||||
console.error('批量导入失败:', error);
|
||||
await this._rollbackExecutedCommands();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _loadImageFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
fabric.Image.fromURL(e.target.result, resolve);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const files = [file1, file2, file3];
|
||||
const importCmd = new ImportMultipleImagesCommand(files, layerManager);
|
||||
const result = await commandManager.execute(importCmd);
|
||||
console.log('导入的图层:', result.importedLayers);
|
||||
```
|
||||
|
||||
### 3. 复制粘贴操作
|
||||
|
||||
```javascript
|
||||
class CopyPasteObjectsCommand extends CompositeCommand {
|
||||
constructor(sourceObjects, targetPosition) {
|
||||
super([], { name: '复制粘贴对象' });
|
||||
this.sourceObjects = sourceObjects;
|
||||
this.targetPosition = targetPosition;
|
||||
this.copiedObjects = [];
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
for (const obj of this.sourceObjects) {
|
||||
// 克隆对象
|
||||
const clonedObj = await this._cloneObject(obj);
|
||||
|
||||
// 设置新位置
|
||||
clonedObj.set({
|
||||
left: this.targetPosition.x + (clonedObj.left - this.sourceObjects[0].left),
|
||||
top: this.targetPosition.y + (clonedObj.top - this.sourceObjects[0].top)
|
||||
});
|
||||
|
||||
// 添加到画布
|
||||
const addCmd = new AddObjectCommand(clonedObj);
|
||||
await addCmd.execute();
|
||||
this.addCommand(addCmd);
|
||||
|
||||
this.copiedObjects.push(clonedObj);
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { copiedObjects: this.copiedObjects };
|
||||
} catch (error) {
|
||||
await this._rollbackExecutedCommands();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _cloneObject(obj) {
|
||||
return new Promise((resolve) => {
|
||||
obj.clone(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ 错误处理策略
|
||||
|
||||
### 1. 复合命令中的错误处理
|
||||
|
||||
```javascript
|
||||
// 推荐的错误处理模式
|
||||
class SafeBatchCommand extends CompositeCommand {
|
||||
constructor(operations, options = {}) {
|
||||
super([], { name: options.name || '安全批量操作' });
|
||||
this.operations = operations;
|
||||
this.stopOnError = options.stopOnError !== false; // 默认遇到错误就停止
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (let i = 0; i < this.operations.length; i++) {
|
||||
const operation = this.operations[i];
|
||||
|
||||
try {
|
||||
const command = new operation.CommandClass(...operation.args);
|
||||
const result = await command.execute();
|
||||
|
||||
this.addCommand(command);
|
||||
results.push({ index: i, success: true, result });
|
||||
|
||||
console.log(`✅ 操作 ${i + 1}/${this.operations.length} 成功`);
|
||||
} catch (error) {
|
||||
errors.push({ index: i, error: error.message });
|
||||
console.error(`❌ 操作 ${i + 1}/${this.operations.length} 失败:`, error);
|
||||
|
||||
if (this.stopOnError) {
|
||||
// 回滚已执行的命令
|
||||
await this._rollbackExecutedCommands();
|
||||
throw new Error(`批量操作在第 ${i + 1} 个操作时失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return {
|
||||
results,
|
||||
errors,
|
||||
successCount: results.length,
|
||||
errorCount: errors.length
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 超时处理
|
||||
|
||||
```javascript
|
||||
class TimeLimitedCommand extends CompositeCommand {
|
||||
constructor(operations, timeout = 5000) {
|
||||
super([], { name: '限时批量操作' });
|
||||
this.operations = operations;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('操作超时')), this.timeout);
|
||||
});
|
||||
|
||||
try {
|
||||
const executePromise = this._executeOperations();
|
||||
const result = await Promise.race([executePromise, timeoutPromise]);
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return result;
|
||||
} catch (error) {
|
||||
await this._rollbackExecutedCommands();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _executeOperations() {
|
||||
for (const operation of this.operations) {
|
||||
const command = new operation.CommandClass(...operation.args);
|
||||
await command.execute();
|
||||
this.addCommand(command);
|
||||
}
|
||||
return { success: true, operationCount: this.operations.length };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 最佳实践
|
||||
|
||||
### 1. 命令粒度控制
|
||||
|
||||
```javascript
|
||||
// ✅ 好的命令粒度 - 逻辑相关的操作组合
|
||||
class CreateUserProfileCommand extends CompositeCommand {
|
||||
constructor(userData) {
|
||||
super([], { name: '创建用户档案' });
|
||||
this.userData = userData;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const avatarCmd = new CreateAvatarCommand(this.userData.avatar);
|
||||
await avatarCmd.execute();
|
||||
this.addCommand(avatarCmd);
|
||||
|
||||
const nameCmd = new CreateNameLabelCommand(this.userData.name);
|
||||
await nameCmd.execute();
|
||||
this.addCommand(nameCmd);
|
||||
|
||||
const infoCmd = new CreateInfoPanelCommand(this.userData.info);
|
||||
await infoCmd.execute();
|
||||
this.addCommand(infoCmd);
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 过大的命令粒度 - 不相关的操作混合
|
||||
// 应该拆分成多个独立的命令
|
||||
```
|
||||
|
||||
### 2. 命名规范
|
||||
|
||||
```javascript
|
||||
// ✅ 清晰的命令命名
|
||||
await commandManager.executeBatch([...], '上传并创建图片图层');
|
||||
new CreateLayerWithObjectsCommand('新图层', objects);
|
||||
new ImportMultipleImagesCommand(files, layerManager);
|
||||
|
||||
// ❌ 模糊的命令命名
|
||||
await commandManager.executeBatch([...], '操作');
|
||||
new BatchCommand();
|
||||
new ProcessCommand();
|
||||
```
|
||||
|
||||
### 3. 性能优化
|
||||
|
||||
```javascript
|
||||
// 对于大量操作,考虑分批处理
|
||||
class BatchProcessor {
|
||||
constructor(commandManager, batchSize = 10) {
|
||||
this.commandManager = commandManager;
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
async processBatch(items, createCommand, progressCallback) {
|
||||
const batches = this._createBatches(items, this.batchSize);
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i];
|
||||
const commands = batch.map(item => createCommand(item));
|
||||
|
||||
await this.commandManager.executeBatch(
|
||||
commands,
|
||||
`批处理 ${i + 1}/${batches.length}`
|
||||
);
|
||||
|
||||
// 更新进度
|
||||
if (progressCallback) {
|
||||
const progress = ((i + 1) / batches.length) * 100;
|
||||
progressCallback(progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createBatches(items, batchSize) {
|
||||
const batches = [];
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
batches.push(items.slice(i, i + batchSize));
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const processor = new BatchProcessor(commandManager, 10);
|
||||
await processor.processBatch(
|
||||
largeItemList,
|
||||
(item) => new ProcessItemCommand(item),
|
||||
(progress) => console.log(`处理进度: ${progress.toFixed(1)}%`)
|
||||
);
|
||||
```
|
||||
|
||||
## 🔧 API 参考
|
||||
|
||||
### CommandManager 核心方法
|
||||
|
||||
```javascript
|
||||
// 执行单个命令
|
||||
await commandManager.execute(command: Command): Promise<any>
|
||||
|
||||
// 批量执行命令
|
||||
await commandManager.executeBatch(commands: Command[], batchName?: string): Promise<any>
|
||||
|
||||
// 撤销/重做
|
||||
await commandManager.undo(): Promise<any>
|
||||
await commandManager.redo(): Promise<any>
|
||||
|
||||
// 状态管理
|
||||
commandManager.getState(): ManagerState
|
||||
commandManager.getHistory(): HistoryInfo
|
||||
commandManager.clear(): void
|
||||
|
||||
// 回调设置
|
||||
commandManager.setChangeCallback(callback: (state: ManagerState) => void): void
|
||||
```
|
||||
|
||||
### CompositeCommand 核心方法
|
||||
|
||||
```javascript
|
||||
// 添加子命令
|
||||
compositeCommand.addCommand(command: Command): CompositeCommand
|
||||
compositeCommand.addCommands(commands: Command[]): CompositeCommand
|
||||
|
||||
// 执行和撤销
|
||||
await compositeCommand.execute(): Promise<any>
|
||||
await compositeCommand.undo(): Promise<any>
|
||||
|
||||
// 获取信息
|
||||
compositeCommand.getInfo(): CommandInfo
|
||||
```
|
||||
|
||||
## 🚨 常见问题
|
||||
|
||||
### Q1: 什么时候使用 executeBatch,什么时候创建自定义复合命令?
|
||||
|
||||
A:
|
||||
- **使用 executeBatch**: 当命令之间没有数据依赖,只是简单的批量执行时
|
||||
- **创建自定义复合命令**: 当需要命令间值传递、条件执行或复杂逻辑时
|
||||
|
||||
```javascript
|
||||
// 简单批量 - 使用 executeBatch
|
||||
await commandManager.executeBatch([
|
||||
new SetPropertyCommand(obj, 'x', 100),
|
||||
new SetPropertyCommand(obj, 'y', 200)
|
||||
], '批量设置属性');
|
||||
|
||||
// 复杂逻辑 - 创建自定义复合命令
|
||||
class CreateLayerWithObjectsCommand extends CompositeCommand {
|
||||
// 需要使用前一个命令的返回值
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 复合命令执行失败时会自动回滚吗?
|
||||
|
||||
A: 是的,`CompositeCommand` 在执行失败时会自动回滚已执行的子命令。
|
||||
|
||||
```javascript
|
||||
// 如果第3个命令失败,前2个命令会被自动撤销
|
||||
const commands = [command1, command2, failingCommand, command4];
|
||||
try {
|
||||
await commandManager.executeBatch(commands);
|
||||
} catch (error) {
|
||||
// 此时 command1 和 command2 已被自动撤销
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 如何处理部分命令失败的情况?
|
||||
|
||||
A: 创建自定义复合命令,设置错误处理策略:
|
||||
|
||||
```javascript
|
||||
class TolerantBatchCommand extends CompositeCommand {
|
||||
constructor(commands, options = {}) {
|
||||
super([], { name: '容错批量命令' });
|
||||
this.originalCommands = commands;
|
||||
this.continueOnError = options.continueOnError || false;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const results = [];
|
||||
|
||||
for (const cmd of this.originalCommands) {
|
||||
try {
|
||||
await cmd.execute();
|
||||
this.addCommand(cmd);
|
||||
results.push({ success: true, command: cmd.constructor.name });
|
||||
} catch (error) {
|
||||
results.push({ success: false, error: error.message });
|
||||
|
||||
if (!this.continueOnError) {
|
||||
await this._rollbackExecutedCommands();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.executedCommands = [...this.commands];
|
||||
return results;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: 如何优化大量命令的性能?
|
||||
|
||||
A: 使用分批处理和进度反馈:
|
||||
|
||||
```javascript
|
||||
// 分批处理大量命令
|
||||
const BATCH_SIZE = 20;
|
||||
const batches = [];
|
||||
|
||||
for (let i = 0; i < largeCommandList.length; i += BATCH_SIZE) {
|
||||
batches.push(largeCommandList.slice(i, i + BATCH_SIZE));
|
||||
}
|
||||
|
||||
for (const [index, batch] of batches.entries()) {
|
||||
await commandManager.executeBatch(batch, `批次 ${index + 1}/${batches.length}`);
|
||||
|
||||
// 给UI更新的机会
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
移除事务机制后的 CommandManager 更加简洁和灵活:
|
||||
|
||||
1. **简化架构**: 去除了不必要的事务状态管理
|
||||
2. **更好的数据流**: 复合命令支持命令间值传递
|
||||
3. **灵活的错误处理**: 自定义复合命令可以实现各种错误处理策略
|
||||
4. **更清晰的API**: `executeBatch` 用于简单批量,自定义复合命令用于复杂逻辑
|
||||
5. **更好的性能**: 减少了状态管理开销
|
||||
|
||||
推荐的使用模式:
|
||||
- **简单批量操作**: 使用 `commandManager.executeBatch()`
|
||||
- **复杂业务逻辑**: 创建继承自 `CompositeCommand` 的自定义命令
|
||||
- **需要值传递**: 在自定义复合命令中处理命令间的数据依赖
|
||||
- **错误处理**: 根据业务需求在复合命令中实现相应的错误处理策略
|
||||
@@ -1238,3 +1238,905 @@ export const ImageUtils = {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 栅格化画布对象为图像
|
||||
* 参考fabric.brushes.js中的convertToImg方法,考虑画布变换参数
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.objects - 要栅格化的对象数组
|
||||
* @param {Object} options.bounds - 边界框 {left, top, width, height} (可选)
|
||||
* @param {boolean} options.trimWhitespace - 是否裁剪空白区域,默认true
|
||||
* @param {number} options.trimPadding - 裁剪时保留的空白边距,默认10像素
|
||||
* @param {number} options.quality - 图像质量 0-1,默认1
|
||||
* @param {string} options.format - 图像格式 'png'|'jpeg',默认'png'
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeCanvasObjects(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
canvas,
|
||||
objects = [],
|
||||
bounds = null,
|
||||
trimWhitespace = true,
|
||||
trimPadding = 10,
|
||||
quality = 1,
|
||||
format = "png",
|
||||
} = options;
|
||||
|
||||
if (!canvas || !Array.isArray(objects)) {
|
||||
reject(new Error("无效的参数:需要画布实例和对象数组"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (objects.length === 0) {
|
||||
reject(new Error("没有对象可栅格化"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用改进的栅格化方法
|
||||
_rasterizeUsingCanvasCopy(canvas, objects, {
|
||||
trimWhitespace,
|
||||
trimPadding,
|
||||
quality,
|
||||
format,
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} catch (error) {
|
||||
console.error("栅格化对象失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用画布复制方式进行栅格化(参考convertToImg实现)
|
||||
* @param {fabric.Canvas} canvas - fabric画布实例
|
||||
* @param {Array} objects - 要栅格化的对象数组
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
trimWhitespace = true,
|
||||
trimPadding = 10,
|
||||
quality = 1,
|
||||
format = "png",
|
||||
} = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalObjects = canvas.getObjects();
|
||||
const originalViewportTransform = [...canvas.viewportTransform];
|
||||
const originalZoom = canvas.getZoom();
|
||||
|
||||
// 临时隐藏其他对象,只显示要栅格化的对象
|
||||
const objectsToHide = originalObjects.filter(
|
||||
(obj) => !objects.includes(obj)
|
||||
);
|
||||
|
||||
// 隐藏不需要的对象
|
||||
objectsToHide.forEach((obj) => {
|
||||
obj._originalVisible = obj.visible;
|
||||
obj.set("visible", false);
|
||||
});
|
||||
|
||||
// 确保要栅格化的对象可见
|
||||
objects.forEach((obj) => {
|
||||
obj._originalVisible = obj.visible;
|
||||
obj.set("visible", true);
|
||||
});
|
||||
|
||||
// 重新渲染画布以应用可见性变化
|
||||
canvas.renderAll();
|
||||
|
||||
// 等待一帧确保渲染完成
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
// 获取画布的像素比例
|
||||
const pixelRatio = canvas.getRetinaScaling();
|
||||
|
||||
// 复制画布元素(这会保持所有变换状态)
|
||||
const copiedCanvas = fabric.util.copyCanvasElement(
|
||||
canvas.lowerCanvasEl
|
||||
);
|
||||
|
||||
let finalCanvas = copiedCanvas;
|
||||
let trimOffset = { x: 0, y: 0 };
|
||||
|
||||
// 裁剪空白区域(如果需要,支持padding)
|
||||
if (trimWhitespace) {
|
||||
const trimResult = _trimCanvas(copiedCanvas, trimPadding);
|
||||
if (trimResult) {
|
||||
finalCanvas = trimResult.canvas;
|
||||
trimOffset = { x: trimResult.offset.x, y: trimResult.offset.y };
|
||||
}
|
||||
}
|
||||
|
||||
// 创建fabric图像对象
|
||||
const fabricImage = new fabric.Image(finalCanvas);
|
||||
|
||||
if (!fabricImage) {
|
||||
throw new Error("创建fabric图像失败");
|
||||
}
|
||||
|
||||
// 获取画布变换参数
|
||||
const pointerX = canvas.viewportTransform[4];
|
||||
const pointerY = canvas.viewportTransform[5];
|
||||
const zoom = canvas.getZoom();
|
||||
|
||||
// 计算最终位置(参考convertToImg的实现)
|
||||
const finalLeft = (trimOffset.x / pixelRatio - pointerX) / zoom;
|
||||
const finalTop = (trimOffset.y / pixelRatio - pointerY) / zoom;
|
||||
const finalScaleX = 1 / pixelRatio / zoom;
|
||||
const finalScaleY = 1 / pixelRatio / zoom;
|
||||
|
||||
// 设置图像属性
|
||||
fabricImage.set({
|
||||
id: generateId("rasterized_image_"),
|
||||
left: finalLeft,
|
||||
top: finalTop,
|
||||
scaleX: finalScaleX,
|
||||
scaleY: finalScaleY,
|
||||
selectable: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
custom: {
|
||||
type: "rasterized",
|
||||
originalObjects: objects.map((obj) => obj.id).filter(Boolean),
|
||||
rasterizedAt: new Date().toISOString(),
|
||||
trimPadding: trimPadding,
|
||||
},
|
||||
});
|
||||
|
||||
fabricImage.setCoords();
|
||||
|
||||
// 恢复对象的原始可见性
|
||||
_restoreObjectVisibility(originalObjects);
|
||||
|
||||
// 重新渲染画布
|
||||
canvas.renderAll();
|
||||
|
||||
resolve(fabricImage);
|
||||
} catch (error) {
|
||||
// 确保恢复对象状态
|
||||
_restoreObjectVisibility(originalObjects);
|
||||
canvas.renderAll();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象的原始可见性状态
|
||||
* @param {Array} objects - 对象数组
|
||||
* @private
|
||||
*/
|
||||
function _restoreObjectVisibility(objects) {
|
||||
objects.forEach((obj) => {
|
||||
if (obj.hasOwnProperty("_originalVisible")) {
|
||||
obj.set("visible", obj._originalVisible);
|
||||
delete obj._originalVisible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 备用栅格化方法:使用toDataURL方式
|
||||
* 当画布复制方法不可用时的备选方案
|
||||
* @param {fabric.Canvas} canvas - fabric画布实例
|
||||
* @param {Array} objects - 要栅格化的对象数组
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
function _rasterizeUsingDataURL(canvas, objects, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const { quality = 1, format = "png" } = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalObjects = canvas.getObjects();
|
||||
|
||||
// 临时移除其他对象
|
||||
const objectsToRemove = originalObjects.filter(
|
||||
(obj) => !objects.includes(obj)
|
||||
);
|
||||
objectsToRemove.forEach((obj) => {
|
||||
canvas.remove(obj);
|
||||
});
|
||||
|
||||
// 重新渲染画布
|
||||
canvas.renderAll();
|
||||
|
||||
// 获取画布数据URL
|
||||
const dataUrl = canvas.toDataURL({
|
||||
format: format,
|
||||
quality: quality,
|
||||
multiplier: canvas.getRetinaScaling(),
|
||||
});
|
||||
|
||||
// 恢复原始对象
|
||||
objectsToRemove.forEach((obj) => {
|
||||
canvas.add(obj);
|
||||
});
|
||||
|
||||
// 恢复原始渲染顺序
|
||||
canvas._objects = [...originalObjects];
|
||||
canvas.renderAll();
|
||||
|
||||
// 创建fabric图像
|
||||
fabric.Image.fromURL(
|
||||
dataUrl,
|
||||
(fabricImage) => {
|
||||
if (!fabricImage) {
|
||||
reject(new Error("创建fabric图像失败"));
|
||||
return;
|
||||
}
|
||||
|
||||
fabricImage.set({
|
||||
id: generateId("rasterized_image_"),
|
||||
left: 0,
|
||||
top: 0,
|
||||
selectable: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
custom: {
|
||||
type: "rasterized",
|
||||
originalObjects: objects.map((obj) => obj.id).filter(Boolean),
|
||||
rasterizedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
fabricImage.setCoords();
|
||||
resolve(fabricImage);
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化画布对象为图像(兼容版本)
|
||||
* 自动选择最适合的栅格化方法
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeCanvasObjectsCompat(options = {}) {
|
||||
const { canvas, objects = [] } = options;
|
||||
|
||||
// 检测是否支持copyCanvasElement
|
||||
if (fabric.util.copyCanvasElement && canvas.lowerCanvasEl) {
|
||||
// 使用画布复制方法(推荐)
|
||||
return rasterizeCanvasObjects(options);
|
||||
} else {
|
||||
// 使用备用方法
|
||||
console.warn("使用备用栅格化方法:toDataURL");
|
||||
return _rasterizeUsingDataURL(canvas, objects, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级栅格化方法:支持更多选项和优化
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.objects - 要栅格化的对象数组
|
||||
* @param {Object} options.bounds - 边界框 {left, top, width, height} (可选)
|
||||
* @param {boolean} options.trimWhitespace - 是否裁剪空白区域,默认true
|
||||
* @param {number} options.quality - 图像质量 0-1,默认1
|
||||
* @param {string} options.format - 图像格式 'png'|'jpeg',默认'png'
|
||||
* @param {boolean} options.preserveObjectState - 是否保持对象状态,默认true
|
||||
* @param {number} options.multiplier - 输出倍数,默认使用画布的retina缩放
|
||||
* @param {boolean} options.useBackgroundColor - 是否使用画布背景色,默认false
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeCanvasObjectsAdvanced(options = {}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
canvas,
|
||||
objects = [],
|
||||
bounds = null,
|
||||
trimWhitespace = true,
|
||||
quality = 1,
|
||||
format = "png",
|
||||
preserveObjectState = true,
|
||||
multiplier = null,
|
||||
useBackgroundColor = false,
|
||||
} = options;
|
||||
|
||||
if (!canvas || !Array.isArray(objects)) {
|
||||
reject(new Error("无效的参数:需要画布实例和对象数组"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (objects.length === 0) {
|
||||
reject(new Error("没有对象可栅格化"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测画布状态
|
||||
const hasTransform =
|
||||
canvas.getZoom() !== 1 ||
|
||||
canvas.viewportTransform[4] !== 0 ||
|
||||
canvas.viewportTransform[5] !== 0;
|
||||
|
||||
let result;
|
||||
|
||||
if (hasTransform && fabric.util.copyCanvasElement) {
|
||||
// 有变换时使用画布复制方法
|
||||
console.log("🎯 检测到画布变换,使用画布复制方法");
|
||||
result = await _rasterizeUsingCanvasCopy(canvas, objects, {
|
||||
trimWhitespace,
|
||||
quality,
|
||||
format,
|
||||
});
|
||||
} else {
|
||||
// 无变换时可以使用更灵活的方法
|
||||
console.log("📐 画布无变换,使用标准栅格化方法");
|
||||
result = await _rasterizeUsingDataURL(canvas, objects, {
|
||||
quality,
|
||||
format,
|
||||
});
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
console.error("高级栅格化失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多个对象的边界框
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {Object} 边界框 {left, top, width, height}
|
||||
* @private
|
||||
*/
|
||||
function _calculateObjectsBounds(objects) {
|
||||
if (!objects || objects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
if (!obj || typeof obj.getBoundingRect !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = obj.getBoundingRect();
|
||||
minX = Math.min(minX, bounds.left);
|
||||
minY = Math.min(minY, bounds.top);
|
||||
maxX = Math.max(maxX, bounds.left + bounds.width);
|
||||
maxY = Math.max(maxY, bounds.top + bounds.height);
|
||||
});
|
||||
|
||||
if (minX === Infinity || minY === Infinity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
left: minX,
|
||||
top: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪画布空白区域(支持保留边距)
|
||||
* 参考fabric.util.trimCanvas方法,添加padding支持
|
||||
* @param {HTMLCanvasElement} canvas - 要裁剪的画布
|
||||
* @param {number} padding - 保留的边距像素,默认0
|
||||
* @returns {Object|null} 裁剪结果 {canvas: 新画布, offset: {x, y}}
|
||||
* @private
|
||||
*/
|
||||
function _trimCanvas(canvas, padding = 0) {
|
||||
try {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const imageData = ctx.getImageData(0, 0, w, h);
|
||||
const pixels = imageData.data;
|
||||
|
||||
let minX = w;
|
||||
let minY = h;
|
||||
let maxX = 0;
|
||||
let maxY = 0;
|
||||
let hasContent = false;
|
||||
|
||||
// 扫描像素找到有内容的区域
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const alpha = pixels[(y * w + x) * 4 + 3];
|
||||
if (alpha > 0) {
|
||||
hasContent = true;
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 应用padding,确保不超出原始画布边界
|
||||
const paddedMinX = Math.max(0, minX - padding);
|
||||
const paddedMinY = Math.max(0, minY - padding);
|
||||
const paddedMaxX = Math.min(w - 1, maxX + padding);
|
||||
const paddedMaxY = Math.min(h - 1, maxY + padding);
|
||||
|
||||
const trimWidth = paddedMaxX - paddedMinX + 1;
|
||||
const trimHeight = paddedMaxY - paddedMinY + 1;
|
||||
|
||||
// 创建裁剪后的画布
|
||||
const trimmedCanvas = document.createElement("canvas");
|
||||
const trimmedCtx = trimmedCanvas.getContext("2d");
|
||||
|
||||
trimmedCanvas.width = trimWidth;
|
||||
trimmedCanvas.height = trimHeight;
|
||||
|
||||
// 复制裁剪区域(包含padding)
|
||||
const trimmedImageData = ctx.getImageData(
|
||||
paddedMinX,
|
||||
paddedMinY,
|
||||
trimWidth,
|
||||
trimHeight
|
||||
);
|
||||
trimmedCtx.putImageData(trimmedImageData, 0, 0);
|
||||
|
||||
return {
|
||||
canvas: trimmedCanvas,
|
||||
offset: { x: paddedMinX, y: paddedMinY },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("裁剪画布失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化图层对象(简化版接口)
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Object} options.layer - 图层对象
|
||||
* @param {boolean} options.includeChildren - 是否包含子图层,默认true
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeLayer(options = {}) {
|
||||
const { canvas, layer, includeChildren = true } = options;
|
||||
|
||||
if (!canvas || !layer) {
|
||||
return Promise.reject(new Error("缺少必要参数:画布或图层"));
|
||||
}
|
||||
|
||||
// 收集图层的所有对象
|
||||
const objects = [];
|
||||
|
||||
if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
|
||||
objects.push(...layer.fabricObjects.filter(Boolean));
|
||||
}
|
||||
|
||||
// 如果包含子图层
|
||||
if (includeChildren && layer.children && Array.isArray(layer.children)) {
|
||||
const collectChildObjects = (childLayer) => {
|
||||
if (childLayer.fabricObjects && Array.isArray(childLayer.fabricObjects)) {
|
||||
objects.push(...childLayer.fabricObjects.filter(Boolean));
|
||||
}
|
||||
if (childLayer.children && Array.isArray(childLayer.children)) {
|
||||
childLayer.children.forEach(collectChildObjects);
|
||||
}
|
||||
};
|
||||
|
||||
layer.children.forEach(collectChildObjects);
|
||||
}
|
||||
|
||||
if (objects.length === 0) {
|
||||
return Promise.reject(new Error("图层没有可栅格化的对象"));
|
||||
}
|
||||
|
||||
// 调用通用栅格化方法
|
||||
return rasterizeCanvasObjects({
|
||||
canvas,
|
||||
objects,
|
||||
trimWhitespace: true,
|
||||
quality: 1,
|
||||
format: "png",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量栅格化多个图层
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.layers - 图层数组
|
||||
* @param {function} options.onProgress - 进度回调
|
||||
* @returns {Promise<Array>} 栅格化结果数组
|
||||
*/
|
||||
export async function batchRasterizeLayers(options = {}) {
|
||||
const { canvas, layers = [], onProgress = null } = options;
|
||||
|
||||
if (!canvas || !Array.isArray(layers)) {
|
||||
throw new Error("缺少必要参数:画布或图层数组");
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const total = layers.length;
|
||||
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
|
||||
try {
|
||||
onProgress?.({ current: i + 1, total, layer, status: "processing" });
|
||||
|
||||
const rasterizedImage = await rasterizeLayer({
|
||||
canvas,
|
||||
layer,
|
||||
includeChildren: true,
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
layer,
|
||||
image: rasterizedImage,
|
||||
layerId: layer.id,
|
||||
});
|
||||
|
||||
onProgress?.({ current: i + 1, total, layer, status: "success" });
|
||||
} catch (error) {
|
||||
console.error(`栅格化图层失败: ${layer.name || layer.id}`, error);
|
||||
|
||||
results.push({
|
||||
success: false,
|
||||
layer,
|
||||
error: error.message,
|
||||
layerId: layer.id,
|
||||
});
|
||||
|
||||
onProgress?.({
|
||||
current: i + 1,
|
||||
total,
|
||||
layer,
|
||||
status: "error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能栅格化:根据对象类型和画布状态自动选择最佳方法
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.objects - 要栅格化的对象数组
|
||||
* @param {Object} options.strategy - 策略配置
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function smartRasterize(options = {}) {
|
||||
const { canvas, objects = [], strategy = {} } = options;
|
||||
|
||||
// 分析对象和画布状态
|
||||
const analysis = _analyzeRasterizationContext(canvas, objects);
|
||||
|
||||
// 选择最佳策略
|
||||
const selectedStrategy = _selectOptimalStrategy(analysis, strategy);
|
||||
|
||||
console.log(`🧠 智能栅格化策略: ${selectedStrategy.method}`, {
|
||||
reason: selectedStrategy.reason,
|
||||
analysis: analysis,
|
||||
});
|
||||
|
||||
// 执行对应的栅格化方法
|
||||
switch (selectedStrategy.method) {
|
||||
case "canvasCopy":
|
||||
return rasterizeCanvasObjects({
|
||||
canvas,
|
||||
objects,
|
||||
...selectedStrategy.options,
|
||||
});
|
||||
|
||||
case "dataURL":
|
||||
return _rasterizeUsingDataURL(canvas, objects, selectedStrategy.options);
|
||||
|
||||
case "advanced":
|
||||
return rasterizeCanvasObjectsAdvanced({
|
||||
canvas,
|
||||
objects,
|
||||
...selectedStrategy.options,
|
||||
});
|
||||
|
||||
default:
|
||||
return rasterizeCanvasObjects({ canvas, objects, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析栅格化上下文
|
||||
* @param {fabric.Canvas} canvas - 画布实例
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {Object} 分析结果
|
||||
* @private
|
||||
*/
|
||||
function _analyzeRasterizationContext(canvas, objects) {
|
||||
const zoom = canvas.getZoom();
|
||||
const viewportTransform = canvas.viewportTransform;
|
||||
const hasTransform =
|
||||
zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
|
||||
|
||||
// 分析对象类型分布
|
||||
const objectTypes = objects.reduce((acc, obj) => {
|
||||
const type = obj.type || "unknown";
|
||||
acc[type] = (acc[type] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 估算复杂度
|
||||
const complexity = _estimateRenderingComplexity(objects);
|
||||
|
||||
// 计算画布利用率
|
||||
const canvasArea = canvas.width * canvas.height;
|
||||
const objectsBounds = _calculateObjectsBounds(objects);
|
||||
const objectsArea = objectsBounds
|
||||
? objectsBounds.width * objectsBounds.height
|
||||
: 0;
|
||||
const utilization = objectsArea / canvasArea;
|
||||
|
||||
return {
|
||||
hasTransform,
|
||||
zoom,
|
||||
objectCount: objects.length,
|
||||
objectTypes,
|
||||
complexity,
|
||||
utilization,
|
||||
canvasSize: { width: canvas.width, height: canvas.height },
|
||||
objectsBounds,
|
||||
supportsCanvasCopy: !!(
|
||||
fabric.util.copyCanvasElement && canvas.lowerCanvasEl
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择最优策略
|
||||
* @param {Object} analysis - 分析结果
|
||||
* @param {Object} userStrategy - 用户指定策略
|
||||
* @returns {Object} 选择的策略
|
||||
* @private
|
||||
*/
|
||||
function _selectOptimalStrategy(analysis, userStrategy = {}) {
|
||||
// 用户指定策略优先
|
||||
if (userStrategy.force) {
|
||||
return {
|
||||
method: userStrategy.force,
|
||||
reason: "用户强制指定",
|
||||
options: userStrategy.options || {},
|
||||
};
|
||||
}
|
||||
|
||||
// 画布变换场景
|
||||
if (analysis.hasTransform && analysis.supportsCanvasCopy) {
|
||||
return {
|
||||
method: "canvasCopy",
|
||||
reason: "画布有变换,使用画布复制方法保持变换状态",
|
||||
options: { trimWhitespace: true },
|
||||
};
|
||||
}
|
||||
|
||||
// 高复杂度场景
|
||||
if (analysis.complexity > 0.8) {
|
||||
return {
|
||||
method: "advanced",
|
||||
reason: "高复杂度渲染,使用高级栅格化方法",
|
||||
options: {
|
||||
preserveObjectState: true,
|
||||
useBackgroundColor: analysis.utilization > 0.5,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 大量对象场景
|
||||
if (analysis.objectCount > 50) {
|
||||
return {
|
||||
method: "dataURL",
|
||||
reason: "大量对象,使用dataURL方法优化性能",
|
||||
options: { quality: 0.9 },
|
||||
};
|
||||
}
|
||||
|
||||
// 低利用率场景(空白较多)
|
||||
if (analysis.utilization < 0.1) {
|
||||
return {
|
||||
method: "canvasCopy",
|
||||
reason: "空白区域较多,使用画布复制+裁剪优化",
|
||||
options: { trimWhitespace: true },
|
||||
};
|
||||
}
|
||||
|
||||
// 默认策略
|
||||
return {
|
||||
method: "canvasCopy",
|
||||
reason: "标准场景,使用画布复制方法",
|
||||
options: { trimWhitespace: true },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算渲染复杂度
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {number} 复杂度分数 0-1
|
||||
* @private
|
||||
*/
|
||||
function _estimateRenderingComplexity(objects) {
|
||||
let complexity = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
let objectComplexity = 0;
|
||||
let weight = 1;
|
||||
|
||||
// 基于对象类型的复杂度
|
||||
switch (obj.type) {
|
||||
case "path":
|
||||
objectComplexity = 0.8;
|
||||
weight = 2;
|
||||
break;
|
||||
case "group":
|
||||
objectComplexity = 0.7;
|
||||
weight = obj.getObjects?.()?.length || 3;
|
||||
break;
|
||||
case "text":
|
||||
case "i-text":
|
||||
case "textbox":
|
||||
objectComplexity = 0.6;
|
||||
weight = (obj.text?.length || 10) / 50;
|
||||
break;
|
||||
case "image":
|
||||
objectComplexity = 0.4;
|
||||
break;
|
||||
case "rect":
|
||||
case "circle":
|
||||
case "ellipse":
|
||||
objectComplexity = 0.2;
|
||||
break;
|
||||
default:
|
||||
objectComplexity = 0.3;
|
||||
}
|
||||
|
||||
// 考虑变换复杂度
|
||||
if (obj.angle && obj.angle !== 0) objectComplexity += 0.1;
|
||||
if (obj.scaleX !== 1 || obj.scaleY !== 1) objectComplexity += 0.1;
|
||||
if (obj.skewX || obj.skewY) objectComplexity += 0.2;
|
||||
|
||||
// 考虑样式复杂度
|
||||
if (obj.shadow) objectComplexity += 0.2;
|
||||
if (obj.stroke) objectComplexity += 0.1;
|
||||
if (obj.strokeDashArray?.length) objectComplexity += 0.1;
|
||||
|
||||
complexity += objectComplexity * weight;
|
||||
totalWeight += weight;
|
||||
});
|
||||
|
||||
return totalWeight > 0 ? Math.min(complexity / totalWeight, 1) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化工具集合
|
||||
* 提供不同场景下的栅格化方法选择
|
||||
*/
|
||||
export const RasterizeUtils = {
|
||||
// 基础栅格化
|
||||
rasterizeCanvasObjects,
|
||||
rasterizeLayer,
|
||||
batchRasterizeLayers,
|
||||
|
||||
// 智能栅格化
|
||||
smartRasterize,
|
||||
|
||||
// 兼容性栅格化
|
||||
rasterizeCanvasObjectsCompat,
|
||||
rasterizeCanvasObjectsAdvanced,
|
||||
|
||||
// 策略栅格化
|
||||
fastRasterize: (canvas, objects) => {
|
||||
return _rasterizeUsingDataURL(canvas, objects, { quality: 0.8 });
|
||||
},
|
||||
|
||||
highQualityRasterize: (canvas, objects) => {
|
||||
return rasterizeCanvasObjectsAdvanced({
|
||||
canvas,
|
||||
objects,
|
||||
quality: 1,
|
||||
trimWhitespace: true,
|
||||
preserveObjectState: true,
|
||||
});
|
||||
},
|
||||
|
||||
compactRasterize: (canvas, objects) => {
|
||||
return rasterizeCanvasObjects({
|
||||
canvas,
|
||||
objects,
|
||||
trimWhitespace: true,
|
||||
format: "jpeg",
|
||||
quality: 0.9,
|
||||
});
|
||||
},
|
||||
|
||||
// 分析工具
|
||||
analyzeRasterizationContext: _analyzeRasterizationContext,
|
||||
estimateComplexity: _estimateRenderingComplexity,
|
||||
calculateObjectsBounds: _calculateObjectsBounds,
|
||||
|
||||
/**
|
||||
* 获取推荐的栅格化方法
|
||||
* @param {fabric.Canvas} canvas - 画布实例
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {Object} 推荐结果
|
||||
*/
|
||||
getRecommendation: (canvas, objects) => {
|
||||
const analysis = _analyzeRasterizationContext(canvas, objects);
|
||||
const strategy = _selectOptimalStrategy(analysis);
|
||||
|
||||
return {
|
||||
recommendedMethod: strategy.method,
|
||||
reason: strategy.reason,
|
||||
analysis: analysis,
|
||||
alternatives: _getAlternativeMethods(analysis),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取备选方法
|
||||
* @param {Object} analysis - 分析结果
|
||||
* @returns {Array} 备选方法列表
|
||||
* @private
|
||||
*/
|
||||
function _getAlternativeMethods(analysis) {
|
||||
const alternatives = [];
|
||||
|
||||
if (analysis.supportsCanvasCopy) {
|
||||
alternatives.push({
|
||||
method: "canvasCopy",
|
||||
pros: ["保持变换状态", "高质量输出", "自动裁剪"],
|
||||
cons: ["可能较慢"],
|
||||
suitable: "有画布变换或需要高质量输出",
|
||||
});
|
||||
}
|
||||
|
||||
alternatives.push({
|
||||
method: "dataURL",
|
||||
pros: ["性能较好", "兼容性强", "处理大量对象"],
|
||||
cons: ["不保持变换", "可能有质量损失"],
|
||||
suitable: "大量对象或性能优先",
|
||||
});
|
||||
|
||||
alternatives.push({
|
||||
method: "advanced",
|
||||
pros: ["智能优化", "完整功能", "自适应策略"],
|
||||
cons: ["复杂度较高"],
|
||||
suitable: "复杂场景或需要最佳效果",
|
||||
});
|
||||
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,9 @@ export function createLayerFromFabricObject(
|
||||
*/
|
||||
export function createLayer(options = {}) {
|
||||
const id =
|
||||
options.id || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
options.id ||
|
||||
generateId("layer_") ||
|
||||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
return {
|
||||
id: id,
|
||||
// 图层基本属性
|
||||
@@ -471,30 +473,36 @@ export function cloneLayer(layer) {
|
||||
* @returns {Object|null} 包含layer和parent的对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayerRecursively(layers, layerId, parent = null) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
try {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 在当前图层列表中查找
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.id === layerId) {
|
||||
return { layer, parent };
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找子图层
|
||||
if (
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const result = findInChildLayers(layer.children, layerId, layer);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`查找图层 ${layerId} 时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 在当前图层列表中查找
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.id === layerId) {
|
||||
return { layer, parent };
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找子图层
|
||||
if (
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const result = findInChildLayers(layer.children, layerId, layer);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`图层 ${layerId} 未找到`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ export function buildLayerAssociations(layer, canvasObjects) {
|
||||
|
||||
if (layer.clippingMask) {
|
||||
// clippingMask 可能是一个fabricObject或组
|
||||
layer.clippingMask =
|
||||
canvasObjects.find((obj) => obj.id === layer.clippingMask.id) || null;
|
||||
const clippingMaskObj = canvasObjects.find(
|
||||
(obj) => obj.id === layer.clippingMask.id
|
||||
);
|
||||
layer.clippingMask = clippingMaskObj?.toObject?.(["id"]) || null;
|
||||
}
|
||||
|
||||
// 处理多个fabricObjects关联
|
||||
@@ -49,6 +51,7 @@ export function buildLayerAssociations(layer, canvasObjects) {
|
||||
*/
|
||||
export function restoreObjectLayerAssociations(layers, canvasObjects) {
|
||||
if (!layers || !canvasObjects || !isArray(canvasObjects)) return;
|
||||
|
||||
layers.forEach((layer) => {
|
||||
buildLayerAssociations(layer, canvasObjects);
|
||||
// 处理子图层
|
||||
@@ -150,55 +153,181 @@ export function validateLayerAssociations(layers, canvasObjects) {
|
||||
|
||||
/**
|
||||
* 简化layers对象属性,只保留必要的属性
|
||||
* @param {Array} layers 图层数组 simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {Array} 简化后的图层数组
|
||||
*/
|
||||
|
||||
export function simplifyLayers(layers) {
|
||||
if (!layers || !isArray(layers)) {
|
||||
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
|
||||
return [];
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
// 处理图层遮罩
|
||||
// 如果clippingMask是一个fabricObject或组,确保它的id正确 // 因为是fabric对象,所以没办法直接获取id,只能通过序列化获取
|
||||
if (layer.clippingMask) {
|
||||
layer.clippingMask = layer.clippingMask?.id || null;
|
||||
}
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
layer.fabricObject = layer.fabricObject?.id;
|
||||
}
|
||||
// 处理多个fabricObjects
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricObject) => {
|
||||
return fabricObject?.id || null; // 确保每个fabricObject都能转换为对象
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
}
|
||||
// 处理子图层
|
||||
if (layer.children && isArray(layer.children)) {
|
||||
layer.children = simplifyLayers(layer.children);
|
||||
}
|
||||
|
||||
// 只保留必要的属性
|
||||
layer = {
|
||||
return layers.map((layer) => {
|
||||
const simplifiedLayer = {
|
||||
id: layer.id,
|
||||
name: layer.name,
|
||||
visible: layer.visible,
|
||||
locked: layer.locked,
|
||||
opacity: layer.opacity,
|
||||
clippingMask: layer.clippingMask || null,
|
||||
fabricObject: layer.fabricObject || null,
|
||||
fabricObjects: layer.fabricObjects || [],
|
||||
children: layer.children || [],
|
||||
isBackground: layer.isBackground || false,
|
||||
ifFixed: layer.ifFixed || false,
|
||||
isFixed: layer.isFixed || false,
|
||||
clippingMask: layer.clippingMask
|
||||
? {
|
||||
id: layer.clippingMask.id,
|
||||
type: layer.clippingMask.type,
|
||||
}
|
||||
: null,
|
||||
fabricObject: layer.fabricObject
|
||||
? {
|
||||
id: layer.fabricObject.id,
|
||||
type: layer.fabricObject.type,
|
||||
}
|
||||
: null,
|
||||
fabricObjects:
|
||||
layer.fabricObjects && isArray(layer.fabricObjects)
|
||||
? layer.fabricObjects
|
||||
.map((fabricObject) =>
|
||||
fabricObject?.id
|
||||
? {
|
||||
id: fabricObject.id,
|
||||
type: fabricObject.type,
|
||||
}
|
||||
: null
|
||||
)
|
||||
.filter((obj) => obj !== null)
|
||||
: [],
|
||||
children:
|
||||
layer.children && isArray(layer.children)
|
||||
? simplifyLayers(layer.children)
|
||||
: [],
|
||||
};
|
||||
});
|
||||
|
||||
return layers;
|
||||
return simplifiedLayer;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复图层的完整关联关系
|
||||
* @param {Array} simplifiedLayers 简化的图层数组
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
* @returns {Array} 恢复关联后的图层数组
|
||||
*/
|
||||
export function restoreLayers(simplifiedLayers, canvasObjects) {
|
||||
if (!simplifiedLayers || !isArray(simplifiedLayers)) {
|
||||
console.warn("restoreLayers 请传入有效的简化图层数组:", simplifiedLayers);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!canvasObjects || !isArray(canvasObjects)) {
|
||||
console.warn("restoreLayers 请传入有效的画布对象数组:", canvasObjects);
|
||||
return simplifiedLayers;
|
||||
}
|
||||
|
||||
return simplifiedLayers.map((layer) => {
|
||||
const restoredLayer = { ...layer };
|
||||
|
||||
// 恢复clippingMask关联
|
||||
if (layer.clippingMask?.id) {
|
||||
const clippingMaskObj = canvasObjects.find(
|
||||
(obj) => obj.id === layer.clippingMask.id
|
||||
);
|
||||
restoredLayer.clippingMask = clippingMaskObj || null;
|
||||
}
|
||||
|
||||
// 恢复单个fabricObject关联
|
||||
if (layer.fabricObject?.id) {
|
||||
const fabricObj = canvasObjects.find(
|
||||
(obj) => obj.id === layer.fabricObject.id
|
||||
);
|
||||
if (fabricObj) {
|
||||
fabricObj.layerId = layer.id;
|
||||
fabricObj.layerName = layer.name;
|
||||
restoredLayer.fabricObject = fabricObj;
|
||||
} else {
|
||||
restoredLayer.fabricObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复多个fabricObjects关联
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
restoredLayer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricRef) => {
|
||||
const fabricObj = canvasObjects.find(
|
||||
(obj) => obj.id === fabricRef.id
|
||||
);
|
||||
if (fabricObj) {
|
||||
fabricObj.layerId = layer.id;
|
||||
fabricObj.layerName = layer.name;
|
||||
return fabricObj;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
}
|
||||
|
||||
// 递归处理子图层
|
||||
if (layer.children && isArray(layer.children)) {
|
||||
restoredLayer.children = restoreLayers(layer.children, canvasObjects);
|
||||
}
|
||||
|
||||
return restoredLayer;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化图层数据用于保存
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {string} 序列化后的JSON字符串
|
||||
*/
|
||||
export function serializeLayers(layers) {
|
||||
try {
|
||||
const simplified = simplifyLayers(layers);
|
||||
return JSON.stringify(simplified, null, 2);
|
||||
} catch (error) {
|
||||
console.error("序列化图层数据失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化图层数据并恢复关联
|
||||
* @param {string} serializedLayers 序列化的图层JSON字符串
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
* @returns {Array} 恢复关联后的图层数组
|
||||
*/
|
||||
export function deserializeLayers(serializedLayers, canvasObjects) {
|
||||
try {
|
||||
const simplified = JSON.parse(serializedLayers);
|
||||
return restoreLayers(simplified, canvasObjects);
|
||||
} catch (error) {
|
||||
console.error("反序列化图层数据失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的存储快照(用于撤销/重做)
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {Object} 图层快照对象
|
||||
*/
|
||||
export function createLayerSnapshot(layers) {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
data: simplifyLayers(layers),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照恢复图层状态
|
||||
* @param {Object} snapshot 图层快照对象
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
* @returns {Array} 恢复的图层数组
|
||||
*/
|
||||
export function restoreFromSnapshot(snapshot, canvasObjects) {
|
||||
if (!snapshot?.data) {
|
||||
console.warn("无效的图层快照:", snapshot);
|
||||
return [];
|
||||
}
|
||||
|
||||
return restoreLayers(snapshot.data, canvasObjects);
|
||||
}
|
||||
|
||||
339
src/component/Canvas/CanvasEditor/utils/rasterizedImage.js
Normal file
339
src/component/Canvas/CanvasEditor/utils/rasterizedImage.js
Normal file
@@ -0,0 +1,339 @@
|
||||
// 栅格化帮助
|
||||
import { fabric } from "fabric-with-all";
|
||||
/**
|
||||
* 创建栅格化图像
|
||||
* 使用增强版栅格化方法,不受原始画布变换影响
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
export const createRasterizedImage = async ({
|
||||
canvas, // 画布对象 必填
|
||||
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
|
||||
maskObject = null, // 用于裁剪的对象 - 可选
|
||||
trimWhitespace = true, // 是否裁剪空白区域
|
||||
trimPadding = 1, // 裁剪边距
|
||||
quality = 1.0, // 图像质量
|
||||
format = "png", // 图像格式
|
||||
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
||||
} = {}) => {
|
||||
try {
|
||||
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
||||
|
||||
// 确保有对象需要栅格化
|
||||
if (fabricObjects.length === 0) {
|
||||
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 高清倍数
|
||||
const currentZoom = canvas.getZoom?.() || 1;
|
||||
scaleFactor = Math.max(
|
||||
scaleFactor || canvas?.getRetinaScaling?.(),
|
||||
currentZoom
|
||||
);
|
||||
|
||||
scaleFactor = Math.min(scaleFactor, 3); // 最大不能大于3
|
||||
|
||||
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
|
||||
|
||||
// 计算绝对边界框(原始尺寸)和相对边界框(当前缩放后的尺寸)
|
||||
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
|
||||
|
||||
console.log("📏 绝对边界框:", absoluteBounds);
|
||||
console.log("📏 相对边界框:", relativeBounds);
|
||||
|
||||
// 使用绝对边界框创建高质量的离屏渲染
|
||||
const rasterizedImage = await createOffscreenRasterization({
|
||||
canvas,
|
||||
objects: fabricObjects,
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
scaleFactor,
|
||||
maskObject,
|
||||
trimWhitespace,
|
||||
trimPadding,
|
||||
quality,
|
||||
format,
|
||||
currentZoom,
|
||||
isReturenDataURL,
|
||||
});
|
||||
|
||||
if (!rasterizedImage) {
|
||||
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isReturenDataURL) {
|
||||
console.log("✅ 栅格化图像创建成功,返回DataURL");
|
||||
return rasterizedImage; // 返回DataURL
|
||||
}
|
||||
|
||||
// 设置栅格化图像的属性
|
||||
if (rasterizedImage) {
|
||||
rasterizedImage.set({
|
||||
selectable: true,
|
||||
evented: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
custom: {
|
||||
type: "rasterized",
|
||||
rasterizedAt: new Date().toISOString(),
|
||||
objectCount: fabricObjects.length,
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
originalZoom: currentZoom,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ 栅格化图像创建完成`);
|
||||
}
|
||||
|
||||
return rasterizedImage;
|
||||
} catch (error) {
|
||||
console.error("创建栅格化图像失败:", error);
|
||||
throw new Error(`栅格化失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算对象的绝对边界框和相对边界框
|
||||
* @param {Array} fabricObjects fabric对象数组
|
||||
* @returns {Object} 包含绝对边界框和相对边界框的对象
|
||||
*/
|
||||
const calculateBounds = (fabricObjects) => {
|
||||
if (fabricObjects.length === 0) {
|
||||
console.warn("⚠️ 没有对象,无法计算边界框");
|
||||
return { absoluteBounds: null, relativeBounds: null };
|
||||
}
|
||||
|
||||
let absoluteBounds = null;
|
||||
let relativeBounds = null;
|
||||
|
||||
fabricObjects.forEach((obj, index) => {
|
||||
// 获取相对边界框(考虑画布缩放和平移)
|
||||
const relativeBound = obj.getBoundingRect();
|
||||
// 获取绝对边界框(原始大小和位置)
|
||||
const absoluteBound = obj.getBoundingRect(true, true);
|
||||
|
||||
console.log(`对象 ${obj.id || index} 边界框比较:`, {
|
||||
relative: relativeBound,
|
||||
absolute: absoluteBound,
|
||||
scaleX: obj.scaleX,
|
||||
scaleY: obj.scaleY,
|
||||
});
|
||||
|
||||
// 计算绝对边界框的累积范围
|
||||
if (!absoluteBounds) {
|
||||
absoluteBounds = { ...absoluteBound };
|
||||
} else {
|
||||
const right = Math.max(
|
||||
absoluteBounds.left + absoluteBounds.width,
|
||||
absoluteBound.left + absoluteBound.width
|
||||
);
|
||||
const bottom = Math.max(
|
||||
absoluteBounds.top + absoluteBounds.height,
|
||||
absoluteBound.top + absoluteBound.height
|
||||
);
|
||||
|
||||
absoluteBounds.left = Math.min(absoluteBounds.left, absoluteBound.left);
|
||||
absoluteBounds.top = Math.min(absoluteBounds.top, absoluteBound.top);
|
||||
absoluteBounds.width = right - absoluteBounds.left;
|
||||
absoluteBounds.height = bottom - absoluteBounds.top;
|
||||
}
|
||||
|
||||
// 计算相对边界框的累积范围
|
||||
if (!relativeBounds) {
|
||||
relativeBounds = { ...relativeBound };
|
||||
} else {
|
||||
const right = Math.max(
|
||||
relativeBounds.left + relativeBounds.width,
|
||||
relativeBound.left + relativeBound.width
|
||||
);
|
||||
const bottom = Math.max(
|
||||
relativeBounds.top + relativeBounds.height,
|
||||
relativeBound.top + relativeBound.height
|
||||
);
|
||||
|
||||
relativeBounds.left = Math.min(relativeBounds.left, relativeBound.left);
|
||||
relativeBounds.top = Math.min(relativeBounds.top, relativeBound.top);
|
||||
relativeBounds.width = right - relativeBounds.left;
|
||||
relativeBounds.height = bottom - relativeBounds.top;
|
||||
}
|
||||
});
|
||||
|
||||
return { absoluteBounds, relativeBounds };
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建离屏栅格化渲染
|
||||
* @param {Object} options 渲染选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
const createOffscreenRasterization = async ({
|
||||
canvas,
|
||||
objects,
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
scaleFactor,
|
||||
maskObject,
|
||||
trimWhitespace,
|
||||
trimPadding,
|
||||
quality,
|
||||
format,
|
||||
currentZoom,
|
||||
isReturenDataURL,
|
||||
}) => {
|
||||
try {
|
||||
// 创建离屏画布,使用绝对尺寸以保证高质量
|
||||
const offscreenCanvas = new fabric.Canvas();
|
||||
|
||||
// 设置离屏画布尺寸为绝对边界框大小,并应用高清倍数
|
||||
const canvasWidth = Math.ceil(absoluteBounds.width * scaleFactor);
|
||||
const canvasHeight = Math.ceil(absoluteBounds.height * scaleFactor);
|
||||
|
||||
offscreenCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
});
|
||||
|
||||
// 设置离屏画布的缩放,确保对象以原始尺寸渲染
|
||||
// offscreenCanvas.setZoom(scaleFactor);
|
||||
|
||||
console.log(
|
||||
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
|
||||
);
|
||||
|
||||
// 克隆对象到离屏画布
|
||||
const clonedObjects = [];
|
||||
for (const obj of objects) {
|
||||
const clonedObj = await cloneObjectAsync(obj);
|
||||
|
||||
// 调整对象位置,相对于绝对边界框的左上角
|
||||
// const absoluteObjBounds = obj.getBoundingRect(true, true);
|
||||
clonedObj.set({
|
||||
left: clonedObj.left - absoluteBounds.left,
|
||||
top: clonedObj.top - absoluteBounds.top,
|
||||
});
|
||||
|
||||
clonedObjects.push(clonedObj);
|
||||
offscreenCanvas.add(clonedObj);
|
||||
}
|
||||
|
||||
// 渲染离屏画布
|
||||
offscreenCanvas.renderAll();
|
||||
|
||||
// 如果有遮罩对象,应用遮罩
|
||||
if (maskObject) {
|
||||
await applyMaskToCanvas(offscreenCanvas, maskObject, absoluteBounds);
|
||||
}
|
||||
|
||||
// 生成图像数据
|
||||
const dataURL = offscreenCanvas.toDataURL({
|
||||
format,
|
||||
quality,
|
||||
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
|
||||
});
|
||||
|
||||
if (isReturenDataURL) {
|
||||
return dataURL; // 如果需要返回DataURL
|
||||
}
|
||||
// 清理离屏画布
|
||||
offscreenCanvas.dispose();
|
||||
|
||||
// 创建fabric.Image对象
|
||||
const fabricImage = await createFabricImageFromDataURL(dataURL);
|
||||
|
||||
// // 应用变换到fabric图像
|
||||
fabricImage.set({
|
||||
...absoluteBounds,
|
||||
});
|
||||
|
||||
return fabricImage;
|
||||
} catch (error) {
|
||||
console.error("离屏栅格化失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 异步克隆fabric对象
|
||||
* @param {fabric.Object} obj 要克隆的对象
|
||||
* @returns {Promise<fabric.Object>} 克隆的对象
|
||||
*/
|
||||
const cloneObjectAsync = (obj) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
obj.clone((cloned) => {
|
||||
if (cloned) {
|
||||
resolve(cloned);
|
||||
} else {
|
||||
reject(new Error("对象克隆失败"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从DataURL创建fabric.Image对象
|
||||
* @param {string} dataURL 图像数据URL
|
||||
* @returns {Promise<fabric.Image>} fabric图像对象
|
||||
*/
|
||||
const createFabricImageFromDataURL = (dataURL) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fabric.Image.fromURL(dataURL, (img) => {
|
||||
if (img) {
|
||||
resolve(img);
|
||||
} else {
|
||||
reject(new Error("无法从DataURL创建图像"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算栅格化图像在当前画布上的正确变换
|
||||
* @param {Object} params 计算参数
|
||||
* @returns {Object} 变换属性对象
|
||||
*/
|
||||
const calculateImageTransform = ({
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
currentZoom,
|
||||
scaleFactor,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
}) => {
|
||||
// 计算缩放比例:相对尺寸 / 绝对尺寸
|
||||
const scaleX = relativeBounds.width / absoluteBounds.width;
|
||||
const scaleY = relativeBounds.height / absoluteBounds.height;
|
||||
|
||||
// 由于我们生成的图像是基于绝对尺寸的高清版本,需要考虑scaleFactor
|
||||
const finalScaleX = scaleX / scaleFactor;
|
||||
const finalScaleY = scaleY / scaleFactor;
|
||||
|
||||
return {
|
||||
left: relativeBounds.left + relativeBounds.width / 2, // 设置为中心点
|
||||
top: relativeBounds.top + relativeBounds.height / 2, // 设置为中心点
|
||||
// scaleX: finalScaleX,
|
||||
// scaleY: finalScaleY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用遮罩到画布(如果需要)
|
||||
* @param {fabric.Canvas} canvas 目标画布
|
||||
* @param {fabric.Object} maskObject 遮罩对象
|
||||
* @param {Object} bounds 边界框
|
||||
*/
|
||||
const applyMaskToCanvas = async (canvas, maskObject, bounds) => {
|
||||
// 这里可以实现遮罩逻辑
|
||||
// 例如使用canvas的clipPath或其他遮罩技术
|
||||
console.log("应用遮罩功能待实现");
|
||||
};
|
||||
|
||||
export const getObjectsBounds = (fabricObjects) => {
|
||||
const { absoluteBounds } = calculateBounds(fabricObjects);
|
||||
return absoluteBounds;
|
||||
};
|
||||
@@ -473,6 +473,11 @@ export default defineComponent({
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.designOpenrtion_print{
|
||||
img{
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.detail_modal_item_front,.designOpenrtion_print{
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
@@ -485,7 +490,6 @@ export default defineComponent({
|
||||
float: left;
|
||||
user-select:none;
|
||||
-webkit-user-drag: none;
|
||||
object-fit: cover;
|
||||
}
|
||||
.modal_imgItem{
|
||||
position: absolute;
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<div v-for="item in speedList"
|
||||
v-show="(type_.type2 == 'Moodboard' && item?.value != 'flux') ||
|
||||
(type_.type2 == 'Sketchboard' && item?.value != 'flux') ||
|
||||
(type_.type2 == 'Printboard')"
|
||||
(type_.type2 == 'Printboard' && item?.value != 'flux')"
|
||||
:key="item.value" :class="{active:item.value == speedData.value}" @click.stop="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
<div class="content" v-show="speedState && scene?.value == 'extract'">
|
||||
@@ -724,7 +724,9 @@ export default defineComponent({
|
||||
element.category = this.scene?.name
|
||||
}
|
||||
}else if(element.status == 'Fail' || element.status == 'Invalid'){
|
||||
console.log(data)
|
||||
data = data.filter((item:any) => item !== element.taskId);
|
||||
console.log(data)
|
||||
this.fileList = this.fileList.filter((item:any) => item.taskId !== element.taskId);
|
||||
message.info(this.t('Generate.everyTimeEffectPoor'));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img_block_item mannquin">
|
||||
<div class="img_block_item mannquin" v-show="probjects.type != 'singleProductDesign'">
|
||||
<div class="title">
|
||||
<div>Mannequin</div>
|
||||
<i class="fi fi-rr-edit" @click="openCollection('mannequin')"></i>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
<div class="collection_page" v-show="isNext">
|
||||
<i v-show="collectionStep > 1" class="fi fi-rr-arrow-small-left" @click="lastStep()"></i>
|
||||
<i v-if="collectionStep < 5" class="fi fi-rr-arrow-small-right Guide_1_8" @click.stop="nextStep()"></i>
|
||||
<i v-if="collectionStep < 4" class="fi fi-rr-arrow-small-right Guide_1_8" @click.stop="nextStep()"></i>
|
||||
<i v-else class="fi fi-rr-check Guide_1_14" @click.stop="cleardata()"></i>
|
||||
|
||||
</div>
|
||||
@@ -133,6 +133,7 @@ export default defineComponent({
|
||||
store.dispatch('getProjectData',str).then((data)=>{
|
||||
if(!data[str])return
|
||||
value[str] = data[str]
|
||||
if(!value[str] || value[str]?.length == 0)return
|
||||
Https.axiosPost(Https.httpUrls.saveModuleContent, value).then((rv)=>{
|
||||
if(rv.boundingBox)store.commit('setShowSketchboard',rv.boundingBox)
|
||||
})
|
||||
@@ -177,11 +178,17 @@ export default defineComponent({
|
||||
let lastStep = async ()=>{
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = data.collectionStep - 1
|
||||
if(data.selectObject.type == 'singleProductDesign' && data.collectionStep == 4){
|
||||
data.collectionStep = data.collectionStep - 1
|
||||
}
|
||||
setOpenSetData()
|
||||
}
|
||||
let nextStep = async ()=>{
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = data.collectionStep + 1
|
||||
if(data.selectObject.type == 'singleProductDesign' && data.collectionStep == 4){
|
||||
data.collectionStep = data.collectionStep + 1
|
||||
}
|
||||
setOpenSetData()
|
||||
}
|
||||
const setOpenSetData = ()=>{
|
||||
|
||||
@@ -589,7 +589,6 @@ methods: {
|
||||
|
||||
}
|
||||
// this.
|
||||
console.log(list[index])
|
||||
this.generateSuccess.userLikeSortId = list[index].userLikeSortId
|
||||
this.generateSuccess.parentId = list[index].parentId
|
||||
this.generateSuccess.productimgIsProductimg = !!this.generateSuccess.productimgIsProductimg
|
||||
@@ -598,7 +597,7 @@ methods: {
|
||||
this.generateSuccess.remPrductimgTime = this.generateSuccess.remPrductimgTime?this.generateSuccess.remPrductimgTime:null
|
||||
this.generateSuccess.prductimgTime = this.generateSuccess.prductimgTime?this.generateSuccess.prductimgTime:null
|
||||
this.scaleImage = true
|
||||
if(this.poseList.length == 0 && list[index].type == 'PoseTransfer'){
|
||||
if(this.poseList.length == 0 && list[index].resultType == 'PoseTransfer'){
|
||||
this.getPoseList()
|
||||
}
|
||||
// if(status == 'edit'){
|
||||
@@ -720,7 +719,12 @@ methods: {
|
||||
}
|
||||
},
|
||||
download(){
|
||||
let url = this.scaleImageList[this.scaleImageIndex].url || this.scaleImageList[this.scaleImageIndex]?.designOutfitUrl
|
||||
let url
|
||||
if(this.generateSuccess.resultType == 'PoseTransfer'){
|
||||
url = this.generateSuccess.newData.videoUrl
|
||||
}else{
|
||||
url = this.generateSuccess.newData.designOutfitUrl || this.generateSuccess?.designOutfitUrl
|
||||
}
|
||||
downloadIamge(url)
|
||||
},
|
||||
setScaleImageIndex(index:any){
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="generage_btn_box">
|
||||
<div class="generage_btn started_btn" style="margin-left: 2rem;">
|
||||
<div class="" @click="openEditTools()" style="margin-left: 1rem;">
|
||||
<div class="" @click.stop="openEditTools()" style="margin-left: 1rem;">
|
||||
Tools
|
||||
</div>
|
||||
<div class="icon iconfont icon-xiala" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
|
||||
@@ -107,6 +107,7 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(fileList)
|
||||
dataDom[value].openSetData(fileList)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ export default defineComponent({
|
||||
}
|
||||
if(dataValue.poseTransfer){
|
||||
let value = {
|
||||
list:dataValue.poseTransfer.list,
|
||||
list:dataValue.poseTransfer.list.filter((rv)=>rv.status != 'Invalid' && rv.status != 'Fail'),
|
||||
likedList:dataValue.poseTransfer.likedList,
|
||||
str:'add',
|
||||
index:-1,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="" v-if="item.status == 'uploading'" style="display: flex;align-items: center;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<img v-show="item.status != 'uploading'" :src="item.imgUrl || item.url" alt="">
|
||||
<img v-show="item.status != 'uploading'" :src="item.designOutfitUrl || item.imgUrl || item.url" alt="">
|
||||
<div v-show="item.status != 'uploading'" class="btnBox">
|
||||
<div :class="{active:item.isChecked}">
|
||||
<i class="fi fi-br-check"></i>
|
||||
@@ -80,7 +80,7 @@
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getgenerate()"></i>
|
||||
<div class="icon iconfont icon-xiala" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click.stop="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isGenerate && !remGenerate" class="generage_btn started_btn">
|
||||
@@ -166,18 +166,6 @@ export default defineComponent({
|
||||
poseList:[],
|
||||
selectPose:null as any,
|
||||
})
|
||||
watch(()=>store.state.HomeStoreModule.uploadElement.length,(newVal,oldVal)=>{
|
||||
data.fileList = store.state.HomeStoreModule.uploadElement
|
||||
data.fileList[0].isChecked = true
|
||||
data.selectImg = data.fileList[0]
|
||||
})
|
||||
watch(()=>store.state.HomeStoreModule.poseTransfer.list.length,(newVal,oldVal)=>{
|
||||
let list = store.state.HomeStoreModule.poseTransfer.list
|
||||
data.isGenerate = true
|
||||
data.remGenerate = true
|
||||
let taskIdList = list.filter((item:any)=>!item.id)
|
||||
if(taskIdList.length > 0)setGenerate(taskIdList[0].taskId)
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
{
|
||||
@@ -216,14 +204,14 @@ export default defineComponent({
|
||||
if(item.url || item.imgUrl)data.selectImg.minioUrl = getMinioUrl(item.url || item.imgUrl)
|
||||
}
|
||||
const openSetData = (designList:any)=>{
|
||||
if(props.isDesignPage){
|
||||
if(props.isDesignPage){//标识design页面打开的tools
|
||||
data.fileList = designList
|
||||
return
|
||||
}else{
|
||||
data.currentList = store.state.UploadFilesModule.modularData.toProduct
|
||||
data.currentList = data.currentList?data.currentList:[]
|
||||
setIsShowMark(false)
|
||||
}
|
||||
// dataDom.generalDrag.openSetData()
|
||||
data.currentList = store.state.UploadFilesModule.modularData.toProduct
|
||||
data.currentList = data.currentList?data.currentList:[]
|
||||
setIsShowMark(false)
|
||||
if(data.poseList.length == 0){
|
||||
getPoseList()
|
||||
}
|
||||
@@ -276,7 +264,8 @@ export default defineComponent({
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.poseTransform,value).then((rv)=>{
|
||||
data.remGenerate = true
|
||||
data.noLikeList.unshift({taskId:rv})
|
||||
data.noLikeList.unshift({taskId:rv,parentId:data.selectImg.parentId})
|
||||
console.log(data.noLikeList)
|
||||
setGenerate(rv)
|
||||
}).catch((res:any)=>{
|
||||
data.isGenerate = false
|
||||
@@ -287,7 +276,6 @@ export default defineComponent({
|
||||
const setGenerate = (dataList:any)=>{
|
||||
let list:any = dataList
|
||||
data.waitList = list
|
||||
let dataNum = dataList.length
|
||||
let state = true
|
||||
data.generateTime = setInterval(()=>{
|
||||
if(!data.isGenerate || !data.remGenerate)return
|
||||
@@ -365,7 +353,7 @@ export default defineComponent({
|
||||
if (!isLt2M) {
|
||||
message.info(useI18n().t('MoodboardUpload.jsContent4'));
|
||||
}
|
||||
if(!data.upload.projectId && !isSelectObject){
|
||||
if(!data?.upload?.projectId && !isSelectObject){
|
||||
isSelectObject = true
|
||||
await createProbject()
|
||||
}
|
||||
@@ -473,9 +461,6 @@ export default defineComponent({
|
||||
item.isChecked = true
|
||||
data.selectPose = item?.id || 1
|
||||
}
|
||||
onMounted(()=>{
|
||||
// showViewVideo({url:'https://www.minio.aida.com.hk:12025/api/v1/download-shared-object/aHR0cHM6Ly93d3cubWluaW8uYWlkYS5jb20uaGs6MTIwMjQvYWlkYS11c2Vycy84OS9wb3NlX3RyYW5zZm9ybV92aWRlby8xMjMtODkubXA0P1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9N0tOVDdNWlNLWkRXM1RVOEJZVlklMkYyMDI1MDQwOCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MDhUMDUxOTM1WiZYLUFtei1FeHBpcmVzPTQzMTk5JlgtQW16LVNlY3VyaXR5LVRva2VuPWV5SmhiR2NpT2lKSVV6VXhNaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpoWTJObGMzTkxaWGtpT2lJM1MwNVVOMDFhVTB0YVJGY3pWRlU0UWxsV1dTSXNJbVY0Y0NJNk1UYzBOREV4T0RneE9Td2ljR0Z5Wlc1MElqb2lZV1J0YVc0aWZRLmY0Z3RoTU1BeC1GUnM3eGhWNFdjTUFCUW5lU19BVkIxUDlYbnJQbEFNWUFsVnJwY3RpYXgtU2cyY2FkZHZ0a0VCOU1NbWxGeUlIbU90aGhUWDlqN2lnJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZ2ZXJzaW9uSWQ9bnVsbCZYLUFtei1TaWduYXR1cmU9Yjg5YmQ4ZDg5M2I4ZjBjYmYxZDI3NDFjZmY0NGRiZGNmYWM2NmU0ZGM2OGIwYzQzZDA2OGI4YjYzZjE5YjhhOA'})
|
||||
})
|
||||
const openSpeed = ()=>{
|
||||
speed.speedState = !speed.speedState
|
||||
if(speed.speedState){
|
||||
@@ -500,6 +485,19 @@ export default defineComponent({
|
||||
store.commit('setUploadElement',storeData)
|
||||
})
|
||||
}
|
||||
watch(()=>store.state.Workspace.probjects.id,(newVal,oldVal)=>{
|
||||
nextTick(()=>{
|
||||
data.fileList = store.state.HomeStoreModule.uploadElement
|
||||
let list = store.state.HomeStoreModule.poseTransfer.list
|
||||
let taskIdList = list.filter((item:any)=>!item.id)
|
||||
if(taskIdList.length > 0){
|
||||
data.isGenerate = true
|
||||
data.remGenerate = true
|
||||
console.log(taskIdList)
|
||||
setGenerate(taskIdList[0].taskId)
|
||||
}
|
||||
})
|
||||
},{immediate: true })
|
||||
return{
|
||||
...toRefs(speed),
|
||||
...toRefs(dataDom),
|
||||
|
||||
@@ -289,9 +289,9 @@ export default defineComponent({
|
||||
},
|
||||
})
|
||||
//watch立即执行一次
|
||||
// watch(()=>productImgData.selectObject.id,(newVal,oldVal)=>{
|
||||
// createProbject.generateList = []
|
||||
// },{immediate: true })
|
||||
watch(()=>productImgData.selectObject.id,(newVal,oldVal)=>{
|
||||
createProbject.generateList = []
|
||||
},{immediate: true })
|
||||
watch(() => route.query.id,
|
||||
(query:any, oldQuery:any) => {
|
||||
if(oldQuery && query != oldQuery){
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="zoom" v-if="item.url" @click.stop="()=>$emit('setBtn',item.id,'zoom')">
|
||||
<i class="fi fi-bs-expand-arrows-alt"></i>
|
||||
</div>
|
||||
<div class="copy" v-if="(type == 'Relight' || type == 'ToProductImage' || type == 'PoseTransfer') && isCopy" @click.stop="()=>$emit('setBtn',item.id,'copy')">
|
||||
<div class="copy" v-if="item.url && (type == 'Relight' || type == 'ToProductImage' || type == 'PoseTransfer') && isCopy" @click.stop="()=>$emit('setBtn',item.id,'copy')">
|
||||
<i class="fi fi-sr-copy-alt"></i>
|
||||
</div>
|
||||
<div class="delete" v-if="item.url && isDelete" @click.stop="()=>$emit('setBtn',item.id,'delete')">
|
||||
|
||||
@@ -67,36 +67,36 @@ axios.interceptors.response.use((res) =>{
|
||||
// message.warning(res.data.errMsg)
|
||||
// return Promise.reject(res.data);
|
||||
// }else
|
||||
if(res.config.env.binary){
|
||||
if(res?.config?.env?.binary){
|
||||
let url = binaryToUrl(res.data,res.config.env.binaryType,res)
|
||||
return Promise.resolve({url,data:res.data})
|
||||
}
|
||||
if (res.data) {
|
||||
if (res.data.errCode === 0) {
|
||||
// message.error(res.data.errMsg)
|
||||
return Promise.resolve(res.data.data);
|
||||
} else if(res.data.errCode === 1){
|
||||
message.warning(res.data.errMsg)
|
||||
return Promise.reject(res.data);
|
||||
} else if(res.data.errCode === 2){
|
||||
return Promise.reject(res.data);
|
||||
}else if(res.data.errCode === -1){
|
||||
message.error(res.data.errMsg)
|
||||
return Promise.reject(res.data);
|
||||
if (res?.data) {
|
||||
if (res?.data?.errCode === 0) {
|
||||
// message.error(res?.data?.errMsg)
|
||||
return Promise.resolve(res?.data?.data);
|
||||
} else if(res?.data?.errCode === 1){
|
||||
message.warning(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data);
|
||||
} else if(res?.data?.errCode === 2){
|
||||
return Promise.reject(res?.data);
|
||||
}else if(res?.data?.errCode === -1){
|
||||
message.error(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (res.data.errCode === 0) {
|
||||
message.warning(res.data.errMsg)
|
||||
return Promise.reject(res.data);
|
||||
} else if(res.data.errCode === 1){
|
||||
message.warning(res.data.errMsg)
|
||||
return Promise.reject(res.data);
|
||||
} else if(res.data.errCode === 2){
|
||||
return Promise.reject(res.data);
|
||||
}else if(res.data.errCode === -1){
|
||||
message.error(res.data.errMsg)
|
||||
return Promise.reject(res.data);
|
||||
if (res?.data?.errCode === 0) {
|
||||
message.warning(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data);
|
||||
} else if(res?.data?.errCode === 1){
|
||||
message.warning(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data);
|
||||
} else if(res?.data?.errCode === 2){
|
||||
return Promise.reject(res?.data);
|
||||
}else if(res?.data?.errCode === -1){
|
||||
message.error(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data);
|
||||
}
|
||||
}
|
||||
}, function(error) {
|
||||
|
||||
@@ -368,7 +368,7 @@ const openTypeList = (t)=>{
|
||||
router:'works=all'
|
||||
},{
|
||||
icon:'',
|
||||
label:'Like Works',
|
||||
label:'Liked works',
|
||||
value:'favoriteWorks',
|
||||
router:'works=favoriteWorks'
|
||||
},{
|
||||
@@ -378,7 +378,7 @@ const openTypeList = (t)=>{
|
||||
router:'works=myWorks'
|
||||
},{
|
||||
icon:'',
|
||||
label:'New Year 2025',
|
||||
label:'New years 2025',
|
||||
value:'NewYear_2025',
|
||||
router:'works=NewYear_2025'
|
||||
},
|
||||
|
||||
@@ -432,7 +432,7 @@
|
||||
<div v-for="item in speedList" :class="{active:item.value == speedData.value}"
|
||||
v-show="(selectCode == 'Moodboard' && item?.value != 'flux') ||
|
||||
(selectCode == 'Sketchboard' && item?.value != 'flux') ||
|
||||
(selectCode == 'Printboard')"
|
||||
(selectCode == 'Printboard' && item?.value != 'flux')"
|
||||
:key="item.value" @click.stop="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
<div class="content" v-else v-show="speedState">
|
||||
|
||||
20
src/vite-env.d.ts
vendored
Normal file
20
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// src/vite-env.d.ts
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// 声明所有以 .js 结尾的模块
|
||||
declare module '*.js' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
// 声明 @/ 路径下的模块
|
||||
declare module '@/tool/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
// 声明其他常用路径
|
||||
declare module '@/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user