2025-06-18 11:05:23 +08:00
|
|
|
|
import { fabric } from "fabric-with-all";
|
2025-07-21 01:17:25 +08:00
|
|
|
|
import initAligningGuidelines, {
|
|
|
|
|
|
initCenteringGuidelines,
|
|
|
|
|
|
} from "../utils/helperLine";
|
2025-06-09 10:25:54 +08:00
|
|
|
|
import { ThumbnailManager } from "./ThumbnailManager";
|
|
|
|
|
|
import { ExportManager } from "./ExportManager";
|
|
|
|
|
|
import {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
findLayerRecursively,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
isGroupLayer,
|
|
|
|
|
|
OperationType,
|
|
|
|
|
|
OperationTypes,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
findLayer,
|
|
|
|
|
|
createLayer,
|
|
|
|
|
|
LayerType,
|
|
|
|
|
|
SpecialLayerId,
|
2026-01-05 11:47:36 +08:00
|
|
|
|
BlendMode,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
} from "../utils/layerHelper";
|
2026-01-02 11:24:11 +08:00
|
|
|
|
import { ObjectMoveCommand } from "../commands/ObjectCommands";
|
2025-06-09 10:25:54 +08:00
|
|
|
|
import { AnimationManager } from "./animation/AnimationManager";
|
|
|
|
|
|
import { createCanvas } from "../utils/canvasFactory";
|
|
|
|
|
|
import { CanvasEventManager } from "./events/CanvasEventManager";
|
|
|
|
|
|
import CanvasConfig from "../config/canvasConfig";
|
|
|
|
|
|
import { RedGreenModeManager } from "./RedGreenModeManager";
|
2025-06-18 11:05:23 +08:00
|
|
|
|
import { EraserStateManager } from "./EraserStateManager";
|
2025-07-21 01:17:25 +08:00
|
|
|
|
import {
|
|
|
|
|
|
deepClone,
|
|
|
|
|
|
findObjectById,
|
|
|
|
|
|
generateId,
|
|
|
|
|
|
optimizeCanvasRendering,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
palletToFill,
|
|
|
|
|
|
fillToCssStyle,
|
|
|
|
|
|
calculateRotatedTopLeftDeg,
|
2026-01-06 14:17:04 +08:00
|
|
|
|
calculateCenterPoint,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
createPatternTransform,
|
2026-01-06 14:17:04 +08:00
|
|
|
|
getTransformScaleAngle,
|
2026-01-05 11:47:36 +08:00
|
|
|
|
base64ToCanvas,
|
2026-01-16 10:29:03 +08:00
|
|
|
|
imageAddGapToCanvas,
|
2025-07-21 01:17:25 +08:00
|
|
|
|
} from "../utils/helper";
|
2025-06-18 11:05:23 +08:00
|
|
|
|
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
|
|
|
|
|
import { isFunction } from "lodash-es";
|
2025-06-09 10:25:54 +08:00
|
|
|
|
import {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
restoreObjectLayerAssociations,
|
|
|
|
|
|
simplifyLayers,
|
|
|
|
|
|
validateLayerAssociations,
|
|
|
|
|
|
} from "../utils/layerUtils";
|
2025-06-29 23:29:47 +08:00
|
|
|
|
import { imageModeHandler } from "../utils/imageHelper";
|
2026-01-02 11:24:11 +08:00
|
|
|
|
import { getObjectAlphaToCanvas } from "../utils/objectHelper";
|
2026-01-14 11:26:51 +08:00
|
|
|
|
import { AddLayerCommand, RemoveLayerCommand, ToggleChildLayerVisibilityCommand } from "../commands/LayerCommands";
|
2026-01-02 11:24:11 +08:00
|
|
|
|
import { fa, id } from "element-plus/es/locales.mjs";
|
|
|
|
|
|
import i18n from "@/lang/index.ts";
|
|
|
|
|
|
const {t} = i18n.global;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
export class CanvasManager {
|
|
|
|
|
|
constructor(canvasElement, options) {
|
|
|
|
|
|
this.canvasElement = canvasElement;
|
|
|
|
|
|
this.width = options.width || 1024;
|
|
|
|
|
|
this.height = options.height || 768;
|
|
|
|
|
|
this.backgroundColor = options.backgroundColor || "#ffffff";
|
|
|
|
|
|
this.toolManager = options.toolManager || null; // 工具管理器引用
|
|
|
|
|
|
this.currentZoom = options.currentZoom || { value: 100 };
|
|
|
|
|
|
this.maskLayer = null; // 添加蒙层引用
|
|
|
|
|
|
this.editorMode = CanvasConfig.defaultTool; // 默认编辑器模式
|
|
|
|
|
|
this.layers = options.layers || null; // 图层引用
|
2025-07-16 18:08:20 +08:00
|
|
|
|
this.lastSelectLayerId = options.lastSelectLayerId || null; // 最后选择的图层ID
|
2025-06-09 10:25:54 +08:00
|
|
|
|
this.canvasWidth = options.canvasWidth || this.width; // 画布宽度
|
|
|
|
|
|
this.canvasHeight = options.canvasHeight || this.height; // 画布高度
|
|
|
|
|
|
this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色
|
|
|
|
|
|
this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式
|
2025-06-26 00:37:07 +08:00
|
|
|
|
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
2025-06-26 00:37:07 +08:00
|
|
|
|
this.handleCanvasInit = null; // 画布初始化回调函数
|
2026-01-02 11:24:11 +08:00
|
|
|
|
this.props = options.props || {};
|
2026-01-12 14:07:14 +08:00
|
|
|
|
this.emit = options.emit || (() => {});
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 初始化画布
|
|
|
|
|
|
this.initializeCanvas();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initializeCanvas() {
|
|
|
|
|
|
console.log("fabric.version:", fabric.version);
|
|
|
|
|
|
this.canvas = createCanvas(this.canvasElement, {
|
|
|
|
|
|
width: this.width,
|
|
|
|
|
|
height: this.height,
|
|
|
|
|
|
preserveObjectStacking: true,
|
|
|
|
|
|
enableRetinaScaling: true,
|
|
|
|
|
|
stopContextMenu: true,
|
|
|
|
|
|
fireRightClick: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化动画管理器
|
|
|
|
|
|
this.animationManager = new AnimationManager(this.canvas, {
|
|
|
|
|
|
currentZoom: this.currentZoom,
|
|
|
|
|
|
wheelThrottleTime: 15, // 降低滚轮事件节流时间,提高响应性
|
|
|
|
|
|
defaultEase: "power2.lin",
|
|
|
|
|
|
defaultDuration: 0.3, // 缩短默认动画时间
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化缩略图管理器
|
|
|
|
|
|
this.thumbnailManager = new ThumbnailManager(this.canvas, {
|
|
|
|
|
|
// 可以根据需求自定义选项
|
2025-06-22 13:52:28 +08:00
|
|
|
|
// layerThumbSize: { width: 32, height: 32 },
|
|
|
|
|
|
// elementThumbSize: { width: 32, height: 24 },
|
2025-06-09 10:25:54 +08:00
|
|
|
|
layers: this.layers,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-06-22 13:52:28 +08:00
|
|
|
|
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// 设置画布辅助线
|
|
|
|
|
|
initAligningGuidelines(this.canvas);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// 设置画布中心线
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// initCenteringGuidelines(this.canvas);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化画布事件监听器
|
|
|
|
|
|
this._initCanvasEvents();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 添加图像到指定图层
|
|
|
|
|
|
async addImageToLayer({ targetLayerId, fabricImage, ...options }) {
|
|
|
|
|
|
// 如果图层管理器存在,将图像合并到当前活动图层
|
|
|
|
|
|
if (this.layerManager) {
|
|
|
|
|
|
// 获取当前活动图层
|
|
|
|
|
|
let activeLayer = targetLayerId
|
|
|
|
|
|
? findLayerRecursively(this.layers.value, targetLayerId)?.layer
|
|
|
|
|
|
: this.layerManager.getActiveLayer();
|
|
|
|
|
|
|
|
|
|
|
|
if (activeLayer) {
|
|
|
|
|
|
// 确保新图像具有正确的图层信息
|
|
|
|
|
|
fabricImage.set({
|
|
|
|
|
|
layerId: activeLayer.id,
|
|
|
|
|
|
layerName: activeLayer.name,
|
|
|
|
|
|
id: fabricImage.id || generateId("brush_img_"),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-06-29 23:29:47 +08:00
|
|
|
|
if (options.imageMode) {
|
|
|
|
|
|
imageModeHandler({
|
|
|
|
|
|
imageMode: options.imageMode,
|
|
|
|
|
|
newImage: fabricImage,
|
|
|
|
|
|
canvasWidth: this.canvasWidth.value,
|
|
|
|
|
|
canvasHeight: this.canvasHeight.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 默认居中
|
|
|
|
|
|
fabricImage.set({
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
left: this.canvas.width / 2,
|
|
|
|
|
|
top: this.canvas.height / 2,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 执行高保真合并操作
|
|
|
|
|
|
await this.eventManager?.mergeLayerObjectsForPerformance?.({
|
|
|
|
|
|
fabricImage,
|
|
|
|
|
|
activeLayer,
|
|
|
|
|
|
options,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.thumbnailManager?.generateLayerThumbnail(activeLayer.id);
|
|
|
|
|
|
|
|
|
|
|
|
// 返回true表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("没有活动图层,无法添加图像");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 初始化画布事件监听器
|
|
|
|
|
|
* 设置鼠标事件、键盘事件等
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_initCanvasEvents() {
|
|
|
|
|
|
// 添加笔刷图像转换处理回调
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.canvas.onBrushImageConverted = async (fabricImage) => {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
await this.addImageToLayer({ fabricImage, targetLayerId: null });
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 返回false表示使用默认行为(直接添加到画布)
|
|
|
|
|
|
return false;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
};
|
2026-01-08 14:29:10 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-07-21 01:17:25 +08:00
|
|
|
|
this.eraserStateManager = new EraserStateManager(
|
|
|
|
|
|
this.canvas,
|
|
|
|
|
|
this.layerManager
|
|
|
|
|
|
);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听擦除开始事件
|
|
|
|
|
|
this.canvas.on("erasing:start", () => {
|
|
|
|
|
|
console.log("开始擦除");
|
|
|
|
|
|
this.eraserStateManager.startErasing();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听擦除结束事件
|
|
|
|
|
|
this.canvas.on("erasing:end", async (e) => {
|
|
|
|
|
|
console.log("擦除完成", e.targets);
|
|
|
|
|
|
// 可以在这里保存状态到命令管理器
|
|
|
|
|
|
const affectedObjects = e.targets || [];
|
|
|
|
|
|
const command = this.eraserStateManager.endErasing(affectedObjects);
|
|
|
|
|
|
if (command && this.commandManager) {
|
|
|
|
|
|
await this.commandManager?.executeCommand?.(command);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await command?.execute?.(); // 如果没有命令管理器,直接执行命令
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新交互性
|
2025-07-21 01:17:25 +08:00
|
|
|
|
command &&
|
|
|
|
|
|
(await this.layerManager?.updateLayersObjectsInteractivity?.());
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
2025-07-21 01:17:25 +08:00
|
|
|
|
this.thumbnailManager?.generateLayerThumbnail(
|
|
|
|
|
|
this.layerManager?.activeLayerId?.value
|
|
|
|
|
|
);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 固定图层 的擦除也需要重新生成缩略图 要判断 当前固定图层是否锁定
|
2025-07-21 01:17:25 +08:00
|
|
|
|
const fixedLayer = this.layers?.value?.find(
|
|
|
|
|
|
(layer) => layer.isFixed && !layer.locked
|
|
|
|
|
|
);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 如果有固定图层且未锁定,则生成缩略图
|
|
|
|
|
|
fixedLayer &&
|
|
|
|
|
|
this.isFixedErasable &&
|
|
|
|
|
|
this.thumbnailManager?.generateLayerThumbnail(fixedLayer?.id);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
});
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置编辑器模式
|
|
|
|
|
|
* @param {string} mode 'draw'、'select'或'pan'
|
|
|
|
|
|
*/
|
|
|
|
|
|
toolChanged(mode) {
|
|
|
|
|
|
if (!OperationTypes.includes(mode)) {
|
|
|
|
|
|
console.warn(`不支持的编辑器模式: ${mode}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.editorMode = mode;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已创建事件管理器,更新它的编辑器模式
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.setEditorMode(mode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setToolManager(toolManager) {
|
|
|
|
|
|
this.toolManager = toolManager || null; // 工具管理器引用
|
|
|
|
|
|
|
|
|
|
|
|
// 更新红绿图模式管理器的工具管理器引用
|
|
|
|
|
|
if (this.redGreenModeManager) {
|
|
|
|
|
|
this.redGreenModeManager.toolManager = this.toolManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已创建事件管理器,更新它的工具管理器引用
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.toolManager = this.toolManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLayerManager(layerManager) {
|
|
|
|
|
|
this.layerManager = layerManager || null; // 图层管理器引用
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化导出管理器(需要在图层管理器设置后初始化)
|
|
|
|
|
|
if (this.layerManager) {
|
|
|
|
|
|
this.exportManager = new ExportManager(this, this.layerManager);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新红绿图模式管理器的图层管理器引用
|
|
|
|
|
|
if (this.redGreenModeManager) {
|
|
|
|
|
|
this.redGreenModeManager.layerManager = this.layerManager;
|
|
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
if (this.eraserStateManager) {
|
|
|
|
|
|
this.eraserStateManager.setLayerManager(this.layerManager);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置命令管理器
|
|
|
|
|
|
* @param {Object} commandManager 命令管理器实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
setCommandManager(commandManager) {
|
|
|
|
|
|
this.commandManager = commandManager;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新红绿图模式管理器的命令管理器引用
|
|
|
|
|
|
if (this.redGreenModeManager) {
|
|
|
|
|
|
this.redGreenModeManager.commandManager = this.commandManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置液化管理器
|
|
|
|
|
|
* @param {Object} liquifyManager 液化管理器实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
setLiquifyManager(liquifyManager) {
|
|
|
|
|
|
this.liquifyManager = liquifyManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置选区管理器
|
|
|
|
|
|
* @param {Object} selectionManager 选区管理器实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
setSelectionManager(selectionManager) {
|
|
|
|
|
|
this.selectionManager = selectionManager;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已创建事件管理器,更新它的选区管理器引用
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.selectionManager = this.selectionManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 14:43:43 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置部件选择管理器
|
|
|
|
|
|
* @param {Object} partManager 部件选择管理器实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
setPartManager(partManager) {
|
|
|
|
|
|
this.partManager = partManager;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已创建事件管理器,更新它的部件选择管理器引用
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.partManager = this.partManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 设置红绿图模式管理器
|
|
|
|
|
|
setRedGreenModeManager(redGreenModeManager) {
|
|
|
|
|
|
this.redGreenModeManager = redGreenModeManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setupCanvasEvents(activeElementId, layerManager) {
|
|
|
|
|
|
// 创建画布事件管理器
|
|
|
|
|
|
this.eventManager = new CanvasEventManager(this.canvas, {
|
|
|
|
|
|
toolManager: this.toolManager,
|
|
|
|
|
|
animationManager: this.animationManager,
|
|
|
|
|
|
thumbnailManager: this.thumbnailManager,
|
|
|
|
|
|
editorMode: this.editorMode,
|
|
|
|
|
|
activeElementId: activeElementId,
|
|
|
|
|
|
layerManager: layerManager,
|
|
|
|
|
|
layers: this.layers,
|
2025-07-16 18:08:20 +08:00
|
|
|
|
lastSelectLayerId: this.lastSelectLayerId,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置动画交互效果
|
|
|
|
|
|
this.animationManager.setupInteractionAnimations();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
setupCanvasInitEvent(handleCanvasInit) {
|
|
|
|
|
|
this.handleCanvasInit = handleCanvasInit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
setupLongPress(callback) {
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.setupLongPress(callback);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateSelectedElements(opt, activeElementId) {
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.updateSelectedElements(opt);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const selected = opt.selected[0];
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
activeElementId.value = selected.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearSelectedElements(activeElementId) {
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.clearSelectedElements();
|
|
|
|
|
|
} else if (activeElementId) {
|
|
|
|
|
|
activeElementId.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用动画管理器的缩放方法
|
|
|
|
|
|
animateZoom(point, targetZoom, options = {}) {
|
|
|
|
|
|
this.animationManager.animateZoom(point, targetZoom, options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 应用缩放(为兼容性保留)
|
|
|
|
|
|
_applyZoom(point, zoom, skipUpdate = false) {
|
|
|
|
|
|
this.animationManager._applyZoom(point, zoom, skipUpdate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用动画管理器的平移方法
|
|
|
|
|
|
animatePan(targetPosition, options = {}) {
|
|
|
|
|
|
this.animationManager.animatePan(targetPosition, options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 应用平移(为兼容性保留)
|
|
|
|
|
|
_applyPan(x, y) {
|
|
|
|
|
|
this.animationManager._applyPan(x, y);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 平移到指定元素
|
|
|
|
|
|
panToElement(elementId) {
|
|
|
|
|
|
this.animationManager.panToElement(elementId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置缩放并居中内容
|
|
|
|
|
|
async resetZoom(animated = true) {
|
|
|
|
|
|
// 先重置缩放
|
|
|
|
|
|
await this.animationManager.resetZoom(animated);
|
|
|
|
|
|
|
|
|
|
|
|
// // 重置视图变换以确保元素位置正确
|
|
|
|
|
|
// this._resetViewportTransform();
|
|
|
|
|
|
|
|
|
|
|
|
// // 居中所有画布元素,包括背景层和其他元素
|
|
|
|
|
|
// this.centerAllObjects();
|
|
|
|
|
|
|
|
|
|
|
|
// 重新渲染画布使变更生效
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 01:54:37 +08:00
|
|
|
|
// 设置固定图层可擦除状态
|
|
|
|
|
|
setFixedLayerErasable({ type = "isFixed", flag = false }) {
|
|
|
|
|
|
const layer = this.layers.value.find((layer) => layer[type]);
|
|
|
|
|
|
if (layer) {
|
|
|
|
|
|
// 设置固定图层的可擦除状态
|
|
|
|
|
|
layer.locked = flag;
|
|
|
|
|
|
// 更新画布对象的erasable属性
|
2025-07-21 01:17:25 +08:00
|
|
|
|
const fabricObject = this.canvas
|
|
|
|
|
|
.getObjects()
|
|
|
|
|
|
.find((obj) => obj.id === layer.id);
|
2025-06-24 01:54:37 +08:00
|
|
|
|
if (fabricObject) {
|
|
|
|
|
|
fabricObject.erasable = flag;
|
|
|
|
|
|
fabricObject.set("erasable", flag);
|
|
|
|
|
|
fabricObject.setCoords(); // 更新控制点坐标
|
|
|
|
|
|
this.canvas.renderAll(); // 重新渲染画布
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
async setCanvasSize(width, height) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
this.width = width;
|
|
|
|
|
|
this.height = height;
|
|
|
|
|
|
this.canvas.setWidth(width);
|
|
|
|
|
|
this.canvas.setHeight(height);
|
|
|
|
|
|
|
|
|
|
|
|
// 重置视图变换以确保元素位置正确
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// this._resetViewportTransform();
|
|
|
|
|
|
if (this.canvas.getZoom() !== 1 || this.canvas.viewportTransform[0] !== 1) {
|
|
|
|
|
|
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
|
|
|
|
|
|
await this.resetZoom();
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 居中所有画布元素,包括背景层和其他元素
|
|
|
|
|
|
this.centerAllObjects();
|
|
|
|
|
|
|
2025-07-10 01:01:46 +08:00
|
|
|
|
// // 重新渲染画布使变更生效
|
|
|
|
|
|
// this.canvas.renderAll();
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置视图变换,使元素回到原始位置
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
2025-06-22 13:52:28 +08:00
|
|
|
|
_resetViewportTransform(zoom) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 保存当前缩放值
|
2025-06-22 13:52:28 +08:00
|
|
|
|
const currentZoom = zoom ?? this.canvas.getZoom();
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 重置视图变换,但保留缩放级别
|
|
|
|
|
|
this.canvas.setViewportTransform([currentZoom, 0, 0, currentZoom, 0, 0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 居中所有画布元素
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
|
|
|
|
|
* 这样可以保持对象间的相对位置关系不变
|
2025-06-09 10:25:54 +08:00
|
|
|
|
*/
|
2026-01-02 11:24:11 +08:00
|
|
|
|
async centerAllObjects() {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
if (!this.canvas) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有可见对象(不是背景元素的对象)
|
|
|
|
|
|
const allObjects = this.canvas.getObjects();
|
|
|
|
|
|
if (allObjects.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const visibleObjects = allObjects.filter(
|
|
|
|
|
|
(obj) => obj.visible !== false && !obj.excludeFromExport
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 如果没有可见对象,直接返回
|
|
|
|
|
|
if (visibleObjects.length === 0) return;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 获取背景对象
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// !this.canvas?.clipPath &&
|
|
|
|
|
|
// this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
2025-07-25 21:38:23 +08:00
|
|
|
|
|
2025-06-22 13:52:28 +08:00
|
|
|
|
this.canvas?.clipPath?.set?.({
|
|
|
|
|
|
left: this.width / 2,
|
|
|
|
|
|
top: this.height / 2,
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.canvas?.clipPath?.setCoords?.();
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 如果只有背景层或没有背景层,使用原有逻辑
|
|
|
|
|
|
if (!backgroundObject) {
|
|
|
|
|
|
console.warn("未找到背景层,使用默认居中逻辑");
|
|
|
|
|
|
// 如果只有一个对象且可能是背景,直接居中
|
|
|
|
|
|
if (visibleObjects.length === 1) {
|
|
|
|
|
|
const obj = visibleObjects[0];
|
|
|
|
|
|
obj.set({
|
|
|
|
|
|
left: this.width / 2,
|
|
|
|
|
|
top: this.height / 2,
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
obj.setCoords();
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 记录背景层居中前的位置
|
|
|
|
|
|
const backgroundOldLeft = backgroundObject.left;
|
|
|
|
|
|
const backgroundOldTop = backgroundObject.top;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算画布中心点
|
|
|
|
|
|
const canvasCenterX = this.width / 2;
|
|
|
|
|
|
const canvasCenterY = this.height / 2;
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 设置背景层居中
|
|
|
|
|
|
backgroundObject.set({
|
|
|
|
|
|
left: canvasCenterX,
|
|
|
|
|
|
top: canvasCenterY,
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 计算背景层的偏移量
|
|
|
|
|
|
const deltaX = backgroundObject.left - backgroundOldLeft;
|
|
|
|
|
|
const deltaY = backgroundObject.top - backgroundOldTop;
|
|
|
|
|
|
// 将相同的偏移量应用到所有其他对象上
|
2025-07-21 01:17:25 +08:00
|
|
|
|
const otherObjects = visibleObjects.filter(
|
|
|
|
|
|
(obj) => obj !== backgroundObject
|
|
|
|
|
|
);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
otherObjects.forEach((obj) => {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
obj.set({
|
|
|
|
|
|
left: obj.left + deltaX,
|
|
|
|
|
|
top: obj.top + deltaY,
|
|
|
|
|
|
});
|
|
|
|
|
|
obj.setCoords(); // 更新对象的控制点坐标
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-14 23:42:28 +08:00
|
|
|
|
let isMaskLayer = false;
|
|
|
|
|
|
// 更新蒙层位置
|
|
|
|
|
|
this.layers.value.forEach((layer) => {
|
|
|
|
|
|
if (layer.clippingMask) {
|
|
|
|
|
|
isMaskLayer = true;
|
|
|
|
|
|
// 如果图层有遮罩,更新遮罩位置
|
|
|
|
|
|
layer.clippingMask.left += deltaX;
|
|
|
|
|
|
layer.clippingMask.top += deltaY;
|
2025-07-21 01:17:25 +08:00
|
|
|
|
|
|
|
|
|
|
if (layer.selectObject) {
|
|
|
|
|
|
// 如果有选区 则选区位置也要更新
|
|
|
|
|
|
layer.selectObject.left = layer.clippingMask.left;
|
|
|
|
|
|
layer.selectObject.top = layer.clippingMask.top;
|
|
|
|
|
|
const { object } = findObjectById(
|
|
|
|
|
|
this.canvas,
|
|
|
|
|
|
layer.selectObject?.id
|
|
|
|
|
|
);
|
|
|
|
|
|
object?.set({
|
|
|
|
|
|
left: layer.clippingMask.left,
|
|
|
|
|
|
top: layer.clippingMask.top,
|
|
|
|
|
|
});
|
|
|
|
|
|
object?.setCoords();
|
|
|
|
|
|
}
|
2025-07-14 23:42:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (isMaskLayer) {
|
2025-07-21 01:17:25 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.layerManager?.updateLayersObjectsInteractivity?.(false, {
|
|
|
|
|
|
isMoveing: true,
|
|
|
|
|
|
});
|
2025-07-14 23:42:28 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 如果有背景层,更新蒙层位置
|
|
|
|
|
|
if (backgroundObject && CanvasConfig.isCropBackground) {
|
|
|
|
|
|
this.updateMaskPosition(backgroundObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// 更新颜色层信息
|
2026-01-08 14:29:10 +08:00
|
|
|
|
// const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
|
|
|
|
|
|
// if(colorObject){
|
|
|
|
|
|
// await this.setObjecCliptInfo(colorObject);
|
|
|
|
|
|
// }
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
|
2026-01-05 11:47:36 +08:00
|
|
|
|
if(groupLayer){
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const groupRect = new fabric.Rect({});
|
2026-01-05 11:47:36 +08:00
|
|
|
|
await this.setObjecCliptInfo(groupRect);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
groupLayer.clippingMask = groupRect.toObject();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 重新渲染画布
|
2026-01-02 11:24:11 +08:00
|
|
|
|
this.canvas.renderAll();
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算多个对象的总边界框
|
|
|
|
|
|
* @private
|
|
|
|
|
|
* @param {Array} objects 要计算边界的对象数组
|
|
|
|
|
|
* @return {Object} 边界信息,包含left、top、width、height
|
|
|
|
|
|
*/
|
|
|
|
|
|
_calculateObjectsBounds(objects) {
|
|
|
|
|
|
if (!objects || objects.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
let minX = Infinity;
|
|
|
|
|
|
let minY = Infinity;
|
|
|
|
|
|
let maxX = -Infinity;
|
|
|
|
|
|
let maxY = -Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
objects.forEach((obj) => {
|
|
|
|
|
|
const bound = obj.getBoundingRect();
|
|
|
|
|
|
minX = Math.min(minX, bound.left);
|
|
|
|
|
|
minY = Math.min(minY, bound.top);
|
|
|
|
|
|
maxX = Math.max(maxX, bound.left + bound.width);
|
|
|
|
|
|
maxY = Math.max(maxY, bound.top + bound.height);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
left: minX,
|
|
|
|
|
|
top: minY,
|
|
|
|
|
|
width: maxX - minX,
|
|
|
|
|
|
height: maxY - minY,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setCanvasColor(color) {
|
|
|
|
|
|
this.backgroundColor = color;
|
2025-06-22 13:52:28 +08:00
|
|
|
|
// this.canvas.setBackgroundColor(
|
|
|
|
|
|
// color,
|
|
|
|
|
|
// this.canvas.renderAll.bind(this.canvas)
|
|
|
|
|
|
// );
|
|
|
|
|
|
this.thumbnailManager?.generateLayerThumbnail?.(
|
|
|
|
|
|
this.layers?.value.find((layer) => layer.isBackground)?.id
|
2025-06-09 10:25:54 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 居中背景层
|
|
|
|
|
|
* @param {Object} backgroundLayerObject 背景层对象
|
|
|
|
|
|
* @param {Number} canvasWidth 画布宽度
|
|
|
|
|
|
* @param {Number} canvasHeight 画布高度
|
|
|
|
|
|
*/
|
2026-01-02 11:24:11 +08:00
|
|
|
|
async centerBackgroundLayer(canvasWidth, canvasHeight) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const backgroundLayerObject = this.getBackgroundLayer();
|
|
|
|
|
|
if (!backgroundLayerObject) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// const bgWidth = backgroundLayerObject.width * backgroundLayerObject.scaleX;
|
|
|
|
|
|
// const bgHeight =
|
|
|
|
|
|
// backgroundLayerObject.height * backgroundLayerObject.scaleY;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算居中位置
|
|
|
|
|
|
const left = canvasWidth / 2;
|
|
|
|
|
|
const top = canvasHeight / 2;
|
|
|
|
|
|
|
|
|
|
|
|
backgroundLayerObject.set({
|
|
|
|
|
|
left: left,
|
|
|
|
|
|
top: top,
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
!CanvasConfig.isCropBackground && this.canvas.renderAll(); // 如果不需要裁剪背景层以外的内容,则渲染画布
|
|
|
|
|
|
|
|
|
|
|
|
// 如果需要裁剪背景层以外的内容,则更新蒙层位置
|
|
|
|
|
|
// 创建或更新蒙层
|
|
|
|
|
|
CanvasConfig.isCropBackground &&
|
2025-06-18 11:05:23 +08:00
|
|
|
|
!this.enabledRedGreenMode &&
|
2025-06-09 10:25:54 +08:00
|
|
|
|
this.createOrUpdateMask(backgroundLayerObject);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建或更新蒙层,用于裁剪不可见区域
|
|
|
|
|
|
* @param {Object} backgroundLayerObject 背景层对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
createOrUpdateMask(backgroundLayerObject) {
|
|
|
|
|
|
if (!backgroundLayerObject) return;
|
|
|
|
|
|
|
|
|
|
|
|
const bgWidth = backgroundLayerObject.width * backgroundLayerObject.scaleX;
|
2025-07-21 01:17:25 +08:00
|
|
|
|
const bgHeight =
|
|
|
|
|
|
backgroundLayerObject.height * backgroundLayerObject.scaleY;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const left = backgroundLayerObject.left;
|
|
|
|
|
|
const top = backgroundLayerObject.top;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已经存在蒙层,则更新它
|
|
|
|
|
|
if (this.maskLayer) {
|
|
|
|
|
|
this.canvas.remove(this.maskLayer);
|
|
|
|
|
|
}
|
2026-01-02 11:24:11 +08:00
|
|
|
|
this.canvas.getObjects().forEach((obj) => {
|
|
|
|
|
|
if (obj.id === "canvasMaskLayer") {
|
|
|
|
|
|
this.canvas.remove(obj);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建蒙层 - 使用透明矩形作为裁剪区域
|
|
|
|
|
|
this.maskLayer = new fabric.Rect({
|
2025-06-18 11:05:23 +08:00
|
|
|
|
id: "canvasMaskLayer",
|
2025-06-09 10:25:54 +08:00
|
|
|
|
width: bgWidth,
|
|
|
|
|
|
height: bgHeight,
|
|
|
|
|
|
left: left,
|
|
|
|
|
|
top: top,
|
|
|
|
|
|
fill: "transparent",
|
2025-06-18 11:05:23 +08:00
|
|
|
|
stroke: "transparent",
|
2025-06-09 10:25:54 +08:00
|
|
|
|
strokeWidth: 1,
|
|
|
|
|
|
strokeDashArray: [5, 5],
|
|
|
|
|
|
selectable: false,
|
|
|
|
|
|
evented: false,
|
|
|
|
|
|
hoverCursor: "default",
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 将蒙层添加到画布
|
|
|
|
|
|
this.canvas.add(this.maskLayer);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置蒙层为最顶层
|
|
|
|
|
|
this.maskLayer.bringToFront();
|
|
|
|
|
|
|
|
|
|
|
|
this.canvas.clipPath = new fabric.Rect({
|
|
|
|
|
|
width: bgWidth,
|
|
|
|
|
|
height: bgHeight,
|
2025-06-18 11:05:23 +08:00
|
|
|
|
left: left,
|
|
|
|
|
|
top: top,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
originX: backgroundLayerObject.originX || "left",
|
|
|
|
|
|
originY: backgroundLayerObject.originY || "top",
|
|
|
|
|
|
absolutePositioned: true,
|
2026-01-14 11:26:51 +08:00
|
|
|
|
rx: 15,
|
|
|
|
|
|
ry: 15,
|
2025-06-09 10:25:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
getBackgroundLayer() {
|
|
|
|
|
|
if (!this.canvas) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const backgroundLayer = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.isBackground;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (backgroundLayer) return backgroundLayer;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到背景层,则根据图层ID查找
|
|
|
|
|
|
const backgroundLayerId = this.layers.value.find((layer) => {
|
|
|
|
|
|
return layer.isBackground;
|
|
|
|
|
|
})?.id;
|
|
|
|
|
|
|
|
|
|
|
|
const backgroundLayerByBgLayer = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.isBackground || obj.id === backgroundLayerId;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!backgroundLayerByBgLayer) {
|
2025-07-21 01:17:25 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
|
"CanvasManager.js = >getBackgroundLayer 方法没有找到背景层"
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return backgroundLayerByBgLayer;
|
|
|
|
|
|
}
|
2026-01-02 11:24:11 +08:00
|
|
|
|
getFixedLayerObject() {
|
|
|
|
|
|
if (!this.canvas) return null;
|
|
|
|
|
|
const fixedLayer = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.isFixed;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (fixedLayer) return fixedLayer;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到固定层,则根据图层ID查找
|
|
|
|
|
|
const fixedLayerId = this.layers.value.find((layer) => {
|
|
|
|
|
|
return layer.isFixed;
|
|
|
|
|
|
})?.id;
|
|
|
|
|
|
|
|
|
|
|
|
const fixedLayerByFixedLayer = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.isFixed || obj.id === fixedLayerId;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!fixedLayerByFixedLayer) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
"CanvasManager.js = >getFixedLayerObject 方法没有找到固定层"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fixedLayerByFixedLayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
getBackgroundLayerObject() {
|
|
|
|
|
|
if (!this.canvas) return null;
|
|
|
|
|
|
const backgroundLayer = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.isBackground;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (backgroundLayer) return backgroundLayer;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到背景层,则根据图层ID查找
|
|
|
|
|
|
const backgroundLayerId = this.layers.value.find((layer) => {
|
|
|
|
|
|
return layer.isBackground;
|
|
|
|
|
|
})?.id;
|
|
|
|
|
|
|
|
|
|
|
|
const backgroundLayerByBgLayer = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.isBackground || obj.id === backgroundLayerId;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!backgroundLayerByBgLayer) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
"CanvasManager.js = >getBackgroundLayerObject 方法没有找到背景层"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return backgroundLayerByBgLayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
getLayerObjectById(layerId) {
|
|
|
|
|
|
if (!this.canvas) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const layerObject = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.id === layerId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (layerObject) return layerObject;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到图层对象,则根据图层ID查找
|
|
|
|
|
|
const layerObjectByLayerId = this.canvas.getObjects().find((obj) => {
|
|
|
|
|
|
return obj.id === layerId;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!layerObjectByLayerId) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
"CanvasManager.js = >getLayerObjectById 方法没有找到图层对象"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return layerObjectByLayerId;
|
|
|
|
|
|
}
|
2026-01-06 14:17:04 +08:00
|
|
|
|
getObjectsByIds(ids){
|
|
|
|
|
|
const objects = this.canvas.getObjects().filter((obj) => {
|
|
|
|
|
|
return ids.includes(obj.id);
|
|
|
|
|
|
});
|
|
|
|
|
|
return objects;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 更新蒙层位置
|
|
|
|
|
|
* @param {Object} backgroundLayerObject 背景层对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateMaskPosition(backgroundLayerObject) {
|
2025-07-21 01:17:25 +08:00
|
|
|
|
if (!backgroundLayerObject || !this.maskLayer || !this.canvas.clipPath)
|
|
|
|
|
|
return;
|
2026-01-14 11:26:51 +08:00
|
|
|
|
console.log("backgroundLayerObject");
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const left = backgroundLayerObject.left;
|
|
|
|
|
|
const top = backgroundLayerObject.top;
|
|
|
|
|
|
|
|
|
|
|
|
this.maskLayer.set({
|
|
|
|
|
|
left: left,
|
|
|
|
|
|
top: top,
|
|
|
|
|
|
width: backgroundLayerObject.width * backgroundLayerObject.scaleX,
|
|
|
|
|
|
height: backgroundLayerObject.height * backgroundLayerObject.scaleY,
|
|
|
|
|
|
originX: backgroundLayerObject.originX || "left",
|
|
|
|
|
|
originY: backgroundLayerObject.originY || "top",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.canvas.clipPath.set({
|
|
|
|
|
|
left: left,
|
|
|
|
|
|
top: top,
|
|
|
|
|
|
width: backgroundLayerObject.width * backgroundLayerObject.scaleX,
|
|
|
|
|
|
height: backgroundLayerObject.height * backgroundLayerObject.scaleY,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新指定图层的缩略图
|
|
|
|
|
|
* @param {String} layerId 图层ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateLayerThumbnail(layerId) {
|
2025-06-22 13:52:28 +08:00
|
|
|
|
this.thumbnailManager?.generateLayerThumbnail?.(layerId);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新所有图层和元素的缩略图
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateAllThumbnails() {
|
|
|
|
|
|
// 为所有元素生成缩略图
|
2025-06-22 13:52:28 +08:00
|
|
|
|
this.thumbnailManager?.generateAllLayerThumbnails?.(this.layers.value);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
*
|
|
|
|
|
|
* 更改固定图层的图片
|
|
|
|
|
|
* @param {String} imageUrl 新的图片URL
|
|
|
|
|
|
* @param {Object} options 选项
|
|
|
|
|
|
* @param {String} options.targetLayerType 目标图层类型(background/fixed)
|
|
|
|
|
|
* @param {Boolean} options.undoable 是否可撤销,默认不可撤销
|
|
|
|
|
|
* @return {Object} 执行结果
|
|
|
|
|
|
* */
|
|
|
|
|
|
async changeFixedImage(imageUrl, options = {}) {
|
|
|
|
|
|
if (!this.layerManager) {
|
|
|
|
|
|
console.error("图层管理器未设置,无法更改固定图层图片");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-06-23 09:27:29 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const command = new ChangeFixedImageCommand({
|
|
|
|
|
|
canvas: this.canvas,
|
|
|
|
|
|
layerManager: this.layerManager,
|
|
|
|
|
|
imageUrl: imageUrl,
|
|
|
|
|
|
targetLayerType: options.targetLayerType || "fixed", // background/fixed
|
2025-06-23 09:27:29 +08:00
|
|
|
|
canvasWidth: this.canvasWidth,
|
|
|
|
|
|
canvasHeight: this.canvasHeight,
|
|
|
|
|
|
...options,
|
2025-06-18 11:05:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-21 01:17:25 +08:00
|
|
|
|
command.undoable =
|
|
|
|
|
|
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
const result = (await command?.execute?.()) || {
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
layerId: null,
|
|
|
|
|
|
imageUrl: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const findLayer = this.layers.value.find((fItem) => {
|
|
|
|
|
|
if (options.targetLayerType === "fixed") {
|
|
|
|
|
|
return fItem.isFixed;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
}
|
2025-06-26 00:37:07 +08:00
|
|
|
|
if (options.targetLayerType === "background") {
|
|
|
|
|
|
return fItem.isBackground;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果找到了图层,则生成缩略图
|
|
|
|
|
|
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
this.layerManager?.sortLayers?.();
|
2025-06-26 00:37:07 +08:00
|
|
|
|
return result;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 导出图片
|
|
|
|
|
|
* @param {Object} options 导出选项
|
|
|
|
|
|
* @param {Boolean} options.isContainBg 是否包含背景图层
|
|
|
|
|
|
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
2026-01-05 11:47:36 +08:00
|
|
|
|
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
|
2026-01-14 11:26:51 +08:00
|
|
|
|
* @param {Boolean} options.isPrintTrimsNoRepeat 是否包含印花图层的不平铺
|
|
|
|
|
|
* @param {Boolean} options.isPrintTrimsRepeat 是否包含印花图层的平铺
|
2026-01-15 14:10:05 +08:00
|
|
|
|
* @param {Boolean} options.isContainNormalLayer 是否包含普通图层
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @param {String} options.layerId 导出具体图层ID
|
|
|
|
|
|
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
|
|
|
|
|
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
2025-09-25 13:52:05 +08:00
|
|
|
|
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
2026-01-02 11:24:11 +08:00
|
|
|
|
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
2025-06-09 10:25:54 +08:00
|
|
|
|
* @returns {String} 导出的图片数据URL
|
|
|
|
|
|
*/
|
2025-07-22 20:39:10 +08:00
|
|
|
|
async exportImage(options = {}) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
if (!this.exportManager) {
|
|
|
|
|
|
console.error("导出管理器未初始化,请确保已设置图层管理器");
|
|
|
|
|
|
throw new Error("导出管理器未初始化");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-07-18 01:28:31 +08:00
|
|
|
|
// 如果当前有选中对象,先清除选中状态 否则导出有问题
|
2025-11-07 09:43:09 +08:00
|
|
|
|
// this.canvas.discardActiveObject(); // 清除选中状态
|
|
|
|
|
|
// this.canvas.renderAll(); // 重新渲染画布
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 自动设置红绿图模式相关参数
|
|
|
|
|
|
const enhancedOptions = {
|
2026-01-14 11:26:51 +08:00
|
|
|
|
isPrintTrimsNoRepeat: true,
|
|
|
|
|
|
isPrintTrimsRepeat: true,
|
2026-01-15 14:10:05 +08:00
|
|
|
|
isContainNormalLayer: true,
|
2025-06-18 11:05:23 +08:00
|
|
|
|
...options,
|
|
|
|
|
|
// 如果没有明确指定,则根据当前模式自动设置
|
|
|
|
|
|
restoreOpacityInRedGreen:
|
2025-07-21 01:17:25 +08:00
|
|
|
|
options.restoreOpacityInRedGreen !== undefined
|
|
|
|
|
|
? options.restoreOpacityInRedGreen
|
2025-07-22 21:40:39 +08:00
|
|
|
|
: false, // 默认在红绿图模式下恢复透明度
|
2026-01-08 14:29:10 +08:00
|
|
|
|
// excludedLayers: [SpecialLayerId.SPECIAL_GROUP], // 导出时排除的图层ID数组
|
2025-06-18 11:05:23 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
|
|
|
|
|
|
if (
|
|
|
|
|
|
this.enabledRedGreenMode &&
|
|
|
|
|
|
!options.layerId &&
|
|
|
|
|
|
(!options.layerIdArray || options.layerIdArray.length === 0)
|
|
|
|
|
|
) {
|
|
|
|
|
|
console.log("检测到红绿图模式,自动包含所有普通图层进行导出");
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有非背景、非固定的普通图层ID
|
|
|
|
|
|
const normalLayerIds =
|
|
|
|
|
|
this.layers?.value
|
2025-07-21 01:17:25 +08:00
|
|
|
|
?.filter(
|
2026-01-05 11:47:36 +08:00
|
|
|
|
(layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther && layer.visible
|
2025-07-21 01:17:25 +08:00
|
|
|
|
)
|
2025-06-18 11:05:23 +08:00
|
|
|
|
?.map((layer) => layer.id) || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (normalLayerIds.length > 0) {
|
|
|
|
|
|
enhancedOptions.layerIdArray = normalLayerIds;
|
|
|
|
|
|
console.log("红绿图模式导出图层:", normalLayerIds);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 11:26:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理特殊图层的显示状态
|
|
|
|
|
|
const ptlids = [];
|
|
|
|
|
|
if(!enhancedOptions.isPrintTrimsNoRepeat || !enhancedOptions.isPrintTrimsRepeat){
|
|
|
|
|
|
let layers = this.layers?.value?.find((layer) => layer.isPrintTrimsGroup)?.children || [];
|
|
|
|
|
|
for(let layer of layers){
|
|
|
|
|
|
if(!layer.visible) continue;
|
|
|
|
|
|
let repeat = layer.fabricObjects?.[0]?.fill?.repeat || "no-repeat";
|
|
|
|
|
|
if(typeof repeat !== "string") repeat = "no-repeat";
|
|
|
|
|
|
if(repeat === "no-repeat"){
|
|
|
|
|
|
if(enhancedOptions.isPrintTrimsNoRepeat) continue;
|
|
|
|
|
|
}else{
|
|
|
|
|
|
if(enhancedOptions.isPrintTrimsRepeat) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
ptlids.push(layer.id);
|
|
|
|
|
|
const command = new ToggleChildLayerVisibilityCommand({
|
|
|
|
|
|
canvas: this.canvas,
|
|
|
|
|
|
layers: this.layers,
|
|
|
|
|
|
layerId: layer.id,
|
|
|
|
|
|
layerManager: this.layerManager,
|
|
|
|
|
|
});
|
|
|
|
|
|
await command.execute(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.changeCanvas();
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await this.exportManager.exportImage(enhancedOptions);
|
|
|
|
|
|
// 恢复特殊图层的显示状态
|
|
|
|
|
|
if(ptlids.length > 0){
|
|
|
|
|
|
for(let id of ptlids){
|
|
|
|
|
|
const command = new ToggleChildLayerVisibilityCommand({
|
|
|
|
|
|
canvas: this.canvas,
|
|
|
|
|
|
layers: this.layers,
|
|
|
|
|
|
layerId: id,
|
|
|
|
|
|
layerManager: this.layerManager,
|
|
|
|
|
|
});
|
|
|
|
|
|
await command.execute(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.changeCanvas();
|
|
|
|
|
|
}
|
|
|
|
|
|
return res;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
} catch (error) {
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.warn("CanvasManager导出图片失败:", error);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-06 14:17:04 +08:00
|
|
|
|
/**
|
2026-01-06 16:53:18 +08:00
|
|
|
|
* 导出印花元素颜色信息
|
|
|
|
|
|
* @returns {Object}
|
2026-01-06 14:17:04 +08:00
|
|
|
|
*/
|
2026-01-06 16:53:18 +08:00
|
|
|
|
async exportExtraInfo() {
|
2026-01-06 14:17:04 +08:00
|
|
|
|
// 导出颜色图层信息
|
|
|
|
|
|
const color = await this.exportColorLayer().catch(() => (null));
|
|
|
|
|
|
// 导出印花和元素图层信息
|
|
|
|
|
|
const printTrimsData = await this.exportPrintTrimsLayers().catch(() => ({prints: null, trims: null}));
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
color,
|
|
|
|
|
|
...printTrimsData,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 导出颜色图层
|
|
|
|
|
|
* @returns {Object} 导出的颜色图层数据URL
|
|
|
|
|
|
*/
|
|
|
|
|
|
async exportColorLayer() {
|
|
|
|
|
|
if (!this.exportManager) {
|
|
|
|
|
|
console.warn("导出管理器未初始化,请确保已设置图层管理器");
|
|
|
|
|
|
return Promise.reject("颜色图层不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
const object = this.getLayerObjectById(SpecialLayerId.COLOR);
|
|
|
|
|
|
if(!object){
|
|
|
|
|
|
console.warn("颜色图层不存在,请确保已添加颜色图层");
|
|
|
|
|
|
return Promise.reject("颜色图层不存在");
|
|
|
|
|
|
}
|
2026-01-06 14:17:04 +08:00
|
|
|
|
const css = fillToCssStyle(object.fill)
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const canvas = new fabric.StaticCanvas();
|
|
|
|
|
|
canvas.setDimensions({
|
|
|
|
|
|
width: object.width,
|
|
|
|
|
|
height: object.height,
|
|
|
|
|
|
backgroundColor: null,
|
|
|
|
|
|
imageSmoothingEnabled: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
const cloneObject = await new Promise((resolve, reject) => {
|
|
|
|
|
|
object.clone(resolve);
|
|
|
|
|
|
});
|
|
|
|
|
|
cloneObject.set({
|
|
|
|
|
|
left: canvas.width / 2,
|
|
|
|
|
|
top: canvas.height / 2,
|
|
|
|
|
|
scaleX: 1,
|
|
|
|
|
|
scaleY: 1,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
clipPath: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
canvas.add(cloneObject);
|
|
|
|
|
|
canvas.renderAll();
|
|
|
|
|
|
const base64 = canvas.toDataURL({
|
|
|
|
|
|
format: "png",
|
|
|
|
|
|
quality: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
canvas.clear();
|
2026-01-06 14:17:04 +08:00
|
|
|
|
const color = object.originColor;
|
|
|
|
|
|
return {css, base64, color};
|
2026-01-02 11:24:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-06 14:17:04 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 导出印花和元素图层
|
|
|
|
|
|
*/
|
|
|
|
|
|
async exportPrintTrimsLayers() {
|
2026-01-08 14:29:10 +08:00
|
|
|
|
const glayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
|
|
|
|
|
|
if(!glayer) return Promise.reject("印花和元素图层组不存在");
|
|
|
|
|
|
const ids = glayer.children.map((v) => v.id);
|
|
|
|
|
|
const objects = this.getObjectsByIds(ids);
|
2026-01-06 14:17:04 +08:00
|
|
|
|
const fixedLayerObj = this.getFixedLayerObject();
|
|
|
|
|
|
if(!fixedLayerObj) return Promise.reject("固定图层不存在");
|
|
|
|
|
|
const flWidth = fixedLayerObj.width
|
|
|
|
|
|
const flHeight = fixedLayerObj.height
|
|
|
|
|
|
const flTop = fixedLayerObj.top
|
|
|
|
|
|
const flLeft = fixedLayerObj.left
|
|
|
|
|
|
const flScaleX = fixedLayerObj.scaleX
|
|
|
|
|
|
const flScaleY = fixedLayerObj.scaleY
|
|
|
|
|
|
const prints = [];
|
|
|
|
|
|
const trims = [];
|
|
|
|
|
|
objects.forEach((v) => {
|
2026-01-08 14:29:10 +08:00
|
|
|
|
const sourceData = glayer.children.find((v_) => v_.id === v.id)?.metadata?.sourceData;
|
|
|
|
|
|
if(!sourceData) return;
|
2026-01-06 14:17:04 +08:00
|
|
|
|
const obj = {
|
2026-01-08 14:29:10 +08:00
|
|
|
|
ifSingle: typeof v.fill === "string",
|
|
|
|
|
|
level2Type: sourceData.level2Type,
|
|
|
|
|
|
designType: sourceData.designType,
|
|
|
|
|
|
path: sourceData.path,
|
|
|
|
|
|
minIOPath: sourceData.minIOPath,
|
2026-01-06 14:17:04 +08:00
|
|
|
|
location: [0, 0],
|
|
|
|
|
|
scale: [0, 0],
|
|
|
|
|
|
angle: v.angle,
|
2026-01-08 14:29:10 +08:00
|
|
|
|
name: sourceData.name,
|
|
|
|
|
|
priority: sourceData.priority,
|
|
|
|
|
|
object:{
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
scaleX: 0,//对象的缩放比例
|
|
|
|
|
|
scaleY: 0,//对象的缩放比例
|
|
|
|
|
|
opacity: v.opacity,
|
|
|
|
|
|
angle: v.angle,
|
|
|
|
|
|
flipX: v.flipX,//是否水平翻转
|
|
|
|
|
|
flipY: v.flipY,//是否垂直翻转
|
|
|
|
|
|
blendMode: v.globalCompositeOperation,// 混合模式
|
|
|
|
|
|
gapX: 0,// 平铺模式下的间距
|
|
|
|
|
|
gapY: 0,// 平铺模式下的间距
|
|
|
|
|
|
}
|
2026-01-06 14:17:04 +08:00
|
|
|
|
}
|
2026-01-08 14:29:10 +08:00
|
|
|
|
let left = (v.left - (flLeft - flWidth * flScaleX / 2));
|
|
|
|
|
|
let top = (v.top - (flTop - flHeight * flScaleY / 2));
|
|
|
|
|
|
let width = (v.width * v.scaleX);
|
|
|
|
|
|
let height = (v.height * v.scaleY);
|
|
|
|
|
|
let {x:cx, y:cy} = calculateCenterPoint(width, height, left, top, v.angle);
|
|
|
|
|
|
let oX = (cx-width/2) / flScaleX;
|
|
|
|
|
|
let oY = (cy-height/2) / flScaleY;
|
|
|
|
|
|
let oScaleX = (v.width * v.scaleX) / (flWidth * flScaleX);
|
|
|
|
|
|
let oScaleY = (v.height * v.scaleY) / (flHeight * flScaleY);
|
|
|
|
|
|
// obj.object.width = width;
|
|
|
|
|
|
// obj.object.height = height;
|
|
|
|
|
|
obj.object.top = oY;
|
|
|
|
|
|
obj.object.left = oX;
|
|
|
|
|
|
obj.object.scaleX = oScaleX;
|
|
|
|
|
|
obj.object.scaleY = oScaleY;
|
2026-01-06 14:17:04 +08:00
|
|
|
|
if(obj.ifSingle){
|
2026-01-08 14:29:10 +08:00
|
|
|
|
obj.location = [oX, oY];
|
|
|
|
|
|
obj.scale = [oScaleX, oScaleY];
|
2026-01-06 14:17:04 +08:00
|
|
|
|
}else{
|
|
|
|
|
|
let fill = v.fill;
|
|
|
|
|
|
let fill_ = v.fill_;
|
2026-01-08 14:29:10 +08:00
|
|
|
|
if(!fill || !fill_) return console.warn("印花元素不存在fill或fill_属性");
|
2026-01-06 14:17:04 +08:00
|
|
|
|
let {scale, angle} = getTransformScaleAngle(fill.patternTransform);
|
|
|
|
|
|
let scaleX = scale * 5 * v.fill_.width / flWidth;
|
|
|
|
|
|
let scaleY = scale * 5 * v.fill_.height / flHeight;
|
|
|
|
|
|
let scaleXY = flWidth > flHeight ? scaleX : scaleY;
|
|
|
|
|
|
|
|
|
|
|
|
let left = fill.offsetX + v.fill_.width * scale / 2;
|
|
|
|
|
|
let top = fill.offsetY + v.fill_.height * scale / 2;
|
|
|
|
|
|
|
|
|
|
|
|
obj.scale = [scaleXY, scaleXY];
|
|
|
|
|
|
obj.angle = angle;
|
|
|
|
|
|
obj.location = [left, top];
|
2026-01-07 10:27:48 +08:00
|
|
|
|
obj.gap = [fill_.gapX, fill_.gapY];
|
2026-01-06 14:17:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
if(obj.level2Type === "Pattern"){
|
|
|
|
|
|
prints.push(obj);
|
|
|
|
|
|
}else if(obj.level2Type === "Embroidery"){
|
|
|
|
|
|
trims.push(obj);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-01-06 16:28:08 +08:00
|
|
|
|
// prints.sort((a, b) => a.ifSingle ? 1 : -1);
|
|
|
|
|
|
prints.forEach((v, i) => v.priority = i + 1);
|
|
|
|
|
|
trims.forEach((v, i) => v.priority = i + 1);
|
2026-01-06 14:17:04 +08:00
|
|
|
|
return {prints, trims};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
dispose() {
|
|
|
|
|
|
// 释放导出管理器资源
|
|
|
|
|
|
if (this.exportManager) {
|
|
|
|
|
|
this.exportManager = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 释放事件管理器资源
|
|
|
|
|
|
if (this.eventManager) {
|
|
|
|
|
|
this.eventManager.dispose();
|
|
|
|
|
|
this.eventManager = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 释放动画管理器资源
|
|
|
|
|
|
if (this.animationManager) {
|
|
|
|
|
|
this.animationManager.dispose();
|
|
|
|
|
|
this.animationManager = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 释放缩略图管理器资源
|
|
|
|
|
|
if (this.thumbnailManager) {
|
|
|
|
|
|
this.thumbnailManager.dispose();
|
|
|
|
|
|
this.thumbnailManager = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.canvas) {
|
|
|
|
|
|
this.canvas.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getJSON() {
|
|
|
|
|
|
try {
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// 清除画布中选中状态
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// this.canvas.discardActiveObject();
|
2025-07-14 01:00:23 +08:00
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
|
2026-01-06 15:09:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 排除颜色图层和特殊组图层
|
|
|
|
|
|
const excludedLayers = [SpecialLayerId.COLOR, SpecialLayerId.SPECIAL_GROUP];
|
|
|
|
|
|
this.layers.value.forEach((layer) => {
|
|
|
|
|
|
if(excludedLayers.includes(layer.id)){
|
|
|
|
|
|
excludedLayers.push(...layer.children?.map((child) => child.id));
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = this.canvas.toJSON([
|
|
|
|
|
|
"id",
|
|
|
|
|
|
"type",
|
|
|
|
|
|
"layerId",
|
|
|
|
|
|
"layerName",
|
|
|
|
|
|
"isBackground",
|
|
|
|
|
|
"isLocked",
|
|
|
|
|
|
"isVisible",
|
|
|
|
|
|
"isFixed",
|
|
|
|
|
|
"parentId",
|
|
|
|
|
|
"eraser",
|
|
|
|
|
|
"eraserable",
|
|
|
|
|
|
"erasable",
|
|
|
|
|
|
"customType",
|
|
|
|
|
|
"fill_",
|
|
|
|
|
|
"scaleX",
|
|
|
|
|
|
"scaleY",
|
|
|
|
|
|
"top",
|
|
|
|
|
|
"left",
|
|
|
|
|
|
"width",
|
|
|
|
|
|
"height",
|
|
|
|
|
|
]);
|
|
|
|
|
|
canvas.objects = canvas.objects.filter((v) => !excludedLayers.includes(v.layerId));
|
|
|
|
|
|
|
2025-07-21 01:17:25 +08:00
|
|
|
|
const simplifyLayersData = simplifyLayers(
|
2026-01-06 15:09:02 +08:00
|
|
|
|
JSON.parse(JSON.stringify(this.layers.value)),
|
|
|
|
|
|
excludedLayers
|
2025-07-21 01:17:25 +08:00
|
|
|
|
);
|
2026-01-06 15:09:02 +08:00
|
|
|
|
const data = {
|
|
|
|
|
|
canvas,
|
2025-07-14 23:42:28 +08:00
|
|
|
|
layers: simplifyLayersData, // 简化图层数据
|
2025-06-09 10:25:54 +08:00
|
|
|
|
version: "1.0", // 添加版本信息
|
|
|
|
|
|
timestamp: new Date().toISOString(), // 添加时间戳
|
|
|
|
|
|
canvasWidth: this.canvasWidth.value,
|
|
|
|
|
|
canvasHeight: this.canvasHeight.value,
|
|
|
|
|
|
canvasColor: this.canvasColor.value,
|
2025-06-22 13:52:28 +08:00
|
|
|
|
activeLayerId: this.layerManager?.activeLayerId?.value,
|
2026-01-06 15:09:02 +08:00
|
|
|
|
};
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.log("获取画布JSON数据...", data);
|
2026-01-06 15:09:02 +08:00
|
|
|
|
return JSON.stringify(data);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("获取画布JSON失败:", error);
|
|
|
|
|
|
throw new Error("获取画布JSON失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
loadJSON(json, calllBack) {
|
2025-06-09 10:25:54 +08:00
|
|
|
|
console.log("加载画布JSON数据:", json);
|
|
|
|
|
|
|
|
|
|
|
|
// 确保传入的json是字符串格式
|
|
|
|
|
|
if (typeof json === "object") {
|
|
|
|
|
|
json = JSON.stringify(json);
|
|
|
|
|
|
} else if (typeof json !== "string") {
|
|
|
|
|
|
throw new Error("loadJSON方法需要传入字符串或对象格式的JSON数据");
|
|
|
|
|
|
}
|
|
|
|
|
|
// 解析JSON字符串
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsedJson = JSON.parse(json);
|
2025-07-10 01:01:46 +08:00
|
|
|
|
this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
|
|
|
|
|
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
|
|
|
|
|
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// eslint-disable-next-line no-async-promise-executor
|
2025-06-22 13:52:28 +08:00
|
|
|
|
return new Promise(async (resolve, reject) => {
|
2025-07-14 23:42:28 +08:00
|
|
|
|
const tempLayers = parsedJson?.layers || [];
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const canvasData = parsedJson?.canvas;
|
|
|
|
|
|
|
|
|
|
|
|
if (!tempLayers) {
|
|
|
|
|
|
reject(new Error("JSON数据中缺少layers字段"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!canvasData) {
|
|
|
|
|
|
reject(new Error("JSON数据中缺少canvas字段"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.layers.value = tempLayers;
|
|
|
|
|
|
|
|
|
|
|
|
// this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
|
|
|
|
|
// this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
|
|
|
|
|
// this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
|
|
|
|
|
|
|
|
|
|
|
|
// 重置视图变换以确保元素位置正确
|
2025-06-22 13:52:28 +08:00
|
|
|
|
this._resetViewportTransform(1);
|
2025-07-25 21:38:23 +08:00
|
|
|
|
// let canvasClipPath = null;
|
|
|
|
|
|
// // 克隆当前裁剪路径
|
|
|
|
|
|
// if (this.canvas?.clipPath) {
|
|
|
|
|
|
// canvasClipPath = this.canvas?.clipPath;
|
|
|
|
|
|
// }
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 清除当前画布内容
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// this.canvas.clear(); // 清除画布内容 可以先去掉 这样加载闪动的情况就比较少 如果有问题 可以再打开
|
2025-06-22 13:52:28 +08:00
|
|
|
|
console.log("清除当前画布内容", canvasData);
|
|
|
|
|
|
delete canvasData.clipPath; // 删除当前裁剪路径
|
2025-06-09 10:25:54 +08:00
|
|
|
|
// 加载画布数据
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.canvas.loadFromJSON(canvasData, async () => {
|
|
|
|
|
|
await optimizeCanvasRendering(this.canvas, async () => {
|
2025-06-22 13:52:28 +08:00
|
|
|
|
// 清空重做栈
|
|
|
|
|
|
this.commandManager?.clear?.();
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
2025-07-25 21:38:23 +08:00
|
|
|
|
// 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;
|
|
|
|
|
|
// }
|
2025-06-18 11:05:23 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 重置画布数据
|
2026-01-02 11:24:11 +08:00
|
|
|
|
await this.setCanvasSize(this.canvas.width, this.canvas.height);
|
|
|
|
|
|
await this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
|
|
|
|
|
await this.createOtherLayers(this.props.otherData);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 重新构建对象关系
|
2025-07-14 23:42:28 +08:00
|
|
|
|
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 验证图层关联关系 - 稳定后可以注释
|
2025-07-14 23:42:28 +08:00
|
|
|
|
// const isValidate = validateLayerAssociations(
|
|
|
|
|
|
// this.layers.value,
|
|
|
|
|
|
// this.canvas.getObjects()
|
|
|
|
|
|
// );
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-07-14 23:42:28 +08:00
|
|
|
|
// console.log("图层关联验证结果:", isValidate);
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 排序
|
|
|
|
|
|
// 使用LayerSort工具重新排列画布对象(如果可用)
|
2026-01-06 15:58:51 +08:00
|
|
|
|
await this?.layerManager?.layerSort?.rearrangeObjects();
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-07-21 01:17:25 +08:00
|
|
|
|
this.layerManager.activeLayerId.value = this.layers.value[0]
|
|
|
|
|
|
.children?.length
|
2025-07-11 00:26:38 +08:00
|
|
|
|
? this.layers.value[0].children[0].id
|
|
|
|
|
|
: this.layers.value[0]?.id || parsedJson?.activeLayerId || null;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// // 如果检测到红绿图模式内容,进行缩放调整
|
|
|
|
|
|
// if (this.enabledRedGreenMode) {
|
|
|
|
|
|
// this._rescaleRedGreenModeContent();
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// 重载代码后支持回调中操作一些内容
|
|
|
|
|
|
|
|
|
|
|
|
// 确保所有对象的交互性正确设置
|
2026-01-02 11:24:11 +08:00
|
|
|
|
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2026-01-09 14:40:18 +08:00
|
|
|
|
await calllBack?.();
|
2026-01-12 14:07:14 +08:00
|
|
|
|
this.emit("canvas-load-json-success");
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 更新所有缩略图
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.updateAllThumbnails();
|
2025-06-22 13:52:28 +08:00
|
|
|
|
}, 500);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
console.log("画布JSON数据加载完成");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 画布初始化事件
|
2025-07-04 03:16:18 +08:00
|
|
|
|
this.handleCanvasInit?.(true);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
resolve();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("恢复图层数据失败:", error);
|
|
|
|
|
|
reject(new Error("恢复图层数据失败: " + error.message));
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
});
|
2025-06-09 10:25:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("解析JSON失败:", error);
|
|
|
|
|
|
throw new Error("解析JSON失败,请检查输入格式: " + error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 创建其他图层:印花、颜色、元素...
|
|
|
|
|
|
* @param {Object} otherData - 其他图层数据
|
|
|
|
|
|
*/
|
2026-01-09 14:40:18 +08:00
|
|
|
|
async createOtherLayers(otherData, isUpdate = false) {
|
2026-01-02 11:24:11 +08:00
|
|
|
|
if (!otherData) return console.warn("otherData 为空不需要添加");
|
|
|
|
|
|
const otherData_ = JSON.parse(JSON.stringify(otherData));
|
|
|
|
|
|
console.log("==========创建其他图层", otherData_);
|
2026-01-06 15:58:51 +08:00
|
|
|
|
|
2026-01-09 14:40:18 +08:00
|
|
|
|
const updateColor = !!otherData_.color;
|
|
|
|
|
|
const updateSpecialGroup = !!otherData_.printObject || !!otherData_.trims;
|
2026-01-06 15:58:51 +08:00
|
|
|
|
// 删除颜色图层和特殊组图层
|
2026-01-09 14:40:18 +08:00
|
|
|
|
const ids = [];
|
|
|
|
|
|
if(isUpdate){
|
|
|
|
|
|
updateColor && ids.push(SpecialLayerId.COLOR)
|
|
|
|
|
|
updateSpecialGroup && ids.push(SpecialLayerId.SPECIAL_GROUP)
|
|
|
|
|
|
}else{
|
|
|
|
|
|
ids.push(SpecialLayerId.COLOR)
|
|
|
|
|
|
ids.push(SpecialLayerId.SPECIAL_GROUP)
|
|
|
|
|
|
}
|
2026-01-06 15:58:51 +08:00
|
|
|
|
this.layers.value = this.layers.value.filter((layer) => {
|
|
|
|
|
|
if(ids.includes(layer.id)){
|
|
|
|
|
|
ids.push(...layer.children?.map((child) => child.id));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})
|
|
|
|
|
|
this.canvas.getObjects().forEach((v) => ids.includes(v.id) && this.canvas.remove(v))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// 创建颜色图层
|
2026-01-09 14:40:18 +08:00
|
|
|
|
otherData_.color && await this.createColorLayer(otherData_.color);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
|
2026-01-06 15:58:51 +08:00
|
|
|
|
const printTrimsLayers = [];// 印花和元素图层
|
|
|
|
|
|
const singleLayers = [];// 平铺图层
|
2026-01-09 14:40:18 +08:00
|
|
|
|
otherData_.printObject?.prints?.forEach((print, index) => {
|
2026-01-06 15:58:51 +08:00
|
|
|
|
print.name = t("Canvas.Print") + (index + 1);
|
|
|
|
|
|
if(print.ifSingle){
|
|
|
|
|
|
printTrimsLayers.unshift({...print});
|
|
|
|
|
|
}else{
|
|
|
|
|
|
singleLayers.unshift({...print});
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-01-09 14:40:18 +08:00
|
|
|
|
otherData_.trims?.prints?.forEach((trims, index) => {
|
2026-01-06 15:58:51 +08:00
|
|
|
|
trims.name = t("Canvas.Elements") + (index + 1);
|
|
|
|
|
|
printTrimsLayers.unshift({...trims});
|
|
|
|
|
|
})
|
2026-01-09 14:40:18 +08:00
|
|
|
|
if(isUpdate ? updateSpecialGroup : true){
|
|
|
|
|
|
await this.createPrintTrimsLayers(printTrimsLayers, singleLayers);
|
|
|
|
|
|
}
|
2026-01-05 11:47:36 +08:00
|
|
|
|
await this.changeCanvas();
|
2026-01-02 11:24:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 11:47:36 +08:00
|
|
|
|
// 设置画布对象的裁剪信息
|
|
|
|
|
|
async setObjecCliptInfo(tagObject, data){
|
|
|
|
|
|
const fixedLayerObj = this.getFixedLayerObject();
|
|
|
|
|
|
if(!fixedLayerObj) return console.warn("固定图层为空");
|
|
|
|
|
|
tagObject.set({
|
2026-01-02 11:24:11 +08:00
|
|
|
|
top: fixedLayerObj.top,
|
|
|
|
|
|
left: fixedLayerObj.left,
|
|
|
|
|
|
width: fixedLayerObj.width,
|
|
|
|
|
|
height: fixedLayerObj.height,
|
|
|
|
|
|
originX: fixedLayerObj.originX,
|
|
|
|
|
|
originY: fixedLayerObj.originY,
|
|
|
|
|
|
scaleX: fixedLayerObj.scaleX,
|
|
|
|
|
|
scaleY: fixedLayerObj.scaleY,
|
|
|
|
|
|
});
|
|
|
|
|
|
var object = fixedLayerObj;
|
|
|
|
|
|
const imageUrl = this.props.clothingImageUrl2;
|
|
|
|
|
|
if(imageUrl){
|
|
|
|
|
|
object = await new Promise((resolve, reject) => {
|
|
|
|
|
|
fabric.Image.fromURL(imageUrl, (imgObject) => {
|
2026-01-05 11:47:36 +08:00
|
|
|
|
tagObject.set({
|
2026-01-02 11:24:11 +08:00
|
|
|
|
width: imgObject.width,
|
|
|
|
|
|
height: imgObject.height,
|
|
|
|
|
|
});
|
|
|
|
|
|
resolve(imgObject);
|
|
|
|
|
|
}, { crossOrigin: "anonymous" });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-05 11:47:36 +08:00
|
|
|
|
const canvas = getObjectAlphaToCanvas(object, data);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const transparentMask = new fabric.Image(canvas, {
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
originX: fixedLayerObj.originX,
|
|
|
|
|
|
originY: fixedLayerObj.originY,
|
|
|
|
|
|
});
|
2026-01-05 11:47:36 +08:00
|
|
|
|
tagObject.set('clipPath', transparentMask);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
}
|
2026-01-09 14:40:18 +08:00
|
|
|
|
async createColorLayer(color_){
|
|
|
|
|
|
const color = color_ || {r:0,g:0,b:0,a:0};
|
2026-01-06 15:58:51 +08:00
|
|
|
|
// if(findLayer(this.layers.value, SpecialLayerId.COLOR)) {
|
|
|
|
|
|
// return console.warn("画布中已存在颜色图层");
|
|
|
|
|
|
// }
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.log("==========添加颜色图层", color, this.layers.value.length)
|
|
|
|
|
|
// 创建颜色图层对象
|
|
|
|
|
|
const colorRect = new fabric.Rect({
|
|
|
|
|
|
id: SpecialLayerId.COLOR,
|
|
|
|
|
|
layerId: SpecialLayerId.COLOR,
|
|
|
|
|
|
layerName: t("Canvas.color"),
|
|
|
|
|
|
isVisible: true,
|
|
|
|
|
|
isLocked: true,
|
2026-01-06 15:58:51 +08:00
|
|
|
|
selectable: false,
|
|
|
|
|
|
hasControls: false,
|
|
|
|
|
|
hasBorders: false,
|
2026-01-05 11:47:36 +08:00
|
|
|
|
globalCompositeOperation: BlendMode.MULTIPLY,
|
2026-01-06 14:17:04 +08:00
|
|
|
|
originColor: color,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
});
|
2026-01-08 14:29:10 +08:00
|
|
|
|
// await this.setObjecCliptInfo(colorRect);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const gradientObj = palletToFill(color);
|
|
|
|
|
|
const gradient = new fabric.Gradient({
|
|
|
|
|
|
type: 'linear',
|
|
|
|
|
|
gradientUnits: 'percentage',
|
|
|
|
|
|
...gradientObj,
|
|
|
|
|
|
})
|
|
|
|
|
|
colorRect.set('fill', gradient);
|
|
|
|
|
|
this.canvas.add(colorRect);
|
|
|
|
|
|
// 创建颜色图层
|
|
|
|
|
|
const colorLayer = createLayer({
|
|
|
|
|
|
id: colorRect.layerId,
|
|
|
|
|
|
name: colorRect.layerName,
|
|
|
|
|
|
type: LayerType.SHAPE,
|
|
|
|
|
|
visible: colorRect.isVisible,
|
|
|
|
|
|
locked: colorRect.isLocked,
|
|
|
|
|
|
opacity: 1.0,
|
|
|
|
|
|
isFixedOther: true,
|
2026-01-05 11:47:36 +08:00
|
|
|
|
blendMode: BlendMode.MULTIPLY,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
fabricObjects: [colorRect.toObject(["id", "layerId", "layerName"])],
|
|
|
|
|
|
})
|
|
|
|
|
|
const groupIndex = this.layers.value.findIndex(layer => layer.isFixed || layer.isBackground);
|
|
|
|
|
|
this.layers.value.splice(groupIndex, 0, colorLayer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建印花和元素图层
|
|
|
|
|
|
async createPrintTrimsLayers(printTrimsLayers, singleLayers){
|
2026-01-06 15:58:51 +08:00
|
|
|
|
// if(findLayer(this.layers.value, SpecialLayerId.SPECIAL_GROUP)) {
|
|
|
|
|
|
// return console.warn("画布中已存在印花和元素组图层");
|
|
|
|
|
|
// }
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.log("==========添加印花和元素图层组", printTrimsLayers, singleLayers)
|
|
|
|
|
|
const fixedLayerObj = this.getFixedLayerObject();
|
|
|
|
|
|
const flWidth = fixedLayerObj.width
|
|
|
|
|
|
const flHeight = fixedLayerObj.height
|
|
|
|
|
|
const flTop = fixedLayerObj.top
|
|
|
|
|
|
const flLeft = fixedLayerObj.left
|
|
|
|
|
|
const flScaleX = fixedLayerObj.scaleX
|
|
|
|
|
|
const flScaleY = fixedLayerObj.scaleY
|
|
|
|
|
|
const children = [];
|
|
|
|
|
|
// 添加印花和元素图层
|
|
|
|
|
|
for(let index = 0; index < printTrimsLayers.length; index++){
|
2026-01-06 14:17:04 +08:00
|
|
|
|
let item = printTrimsLayers[index];
|
2026-01-02 11:24:11 +08:00
|
|
|
|
let id = generateId("layer_image_");
|
2026-01-06 14:17:04 +08:00
|
|
|
|
let name = item.name;
|
2026-01-02 11:24:11 +08:00
|
|
|
|
let image = await new Promise(resolve => {
|
2026-01-06 14:17:04 +08:00
|
|
|
|
fabric.Image.fromURL(item.path, (fabricImage)=>{
|
|
|
|
|
|
const left = flLeft - flWidth * flScaleX / 2 + (item.location?.[0] || 0) * flScaleX
|
|
|
|
|
|
const top = flTop - flHeight * flScaleY / 2 + (item.location?.[1] || 0) * flScaleY
|
|
|
|
|
|
const scaleX = flWidth * (item.scale?.[0] || 1) / fabricImage.width * flScaleX
|
|
|
|
|
|
const scaleY = flHeight * (item.scale?.[1] || 1) / fabricImage.height * flScaleY
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const {x, y} = calculateRotatedTopLeftDeg(
|
|
|
|
|
|
fabricImage.width * scaleX,
|
|
|
|
|
|
fabricImage.height * scaleY,
|
|
|
|
|
|
left,
|
|
|
|
|
|
top,
|
|
|
|
|
|
0,
|
2026-01-06 14:17:04 +08:00
|
|
|
|
item.angle || 0
|
2026-01-02 11:24:11 +08:00
|
|
|
|
)
|
2026-01-06 14:17:04 +08:00
|
|
|
|
const angle = item.angle || 0
|
2026-01-02 11:24:11 +08:00
|
|
|
|
fabricImage.set({
|
|
|
|
|
|
left: x,
|
|
|
|
|
|
top: y,
|
|
|
|
|
|
scaleX: scaleX,
|
|
|
|
|
|
scaleY: scaleY,
|
|
|
|
|
|
angle: angle,
|
|
|
|
|
|
id: id,
|
|
|
|
|
|
layerId: id,
|
|
|
|
|
|
layerName: name,
|
|
|
|
|
|
selectable: true,
|
|
|
|
|
|
hasControls: true,
|
|
|
|
|
|
hasBorders: true,
|
2026-01-14 11:26:51 +08:00
|
|
|
|
isPrintTrims: true,
|
2026-01-08 14:29:10 +08:00
|
|
|
|
globalCompositeOperation: BlendMode.MULTIPLY,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
resolve(fabricImage);
|
|
|
|
|
|
}, { crossOrigin: "anonymous" });
|
|
|
|
|
|
})
|
|
|
|
|
|
this.canvas.add(image);
|
|
|
|
|
|
let layer = createLayer({
|
|
|
|
|
|
id: id,
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
type: LayerType.BITMAP,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
locked: false,
|
|
|
|
|
|
opacity: 1.0,
|
2026-01-14 11:26:51 +08:00
|
|
|
|
isPrintTrims: true,
|
2026-01-08 14:29:10 +08:00
|
|
|
|
blendMode: BlendMode.MULTIPLY,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
fabricObjects: [image.toObject(["id", "layerId", "layerName"])],
|
2026-01-08 14:29:10 +08:00
|
|
|
|
metadata: {sourceData: item},
|
2026-01-02 11:24:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
children.push(layer);
|
|
|
|
|
|
};
|
|
|
|
|
|
// 添加平铺图层
|
|
|
|
|
|
for(let index = 0; index < singleLayers.length; index++){
|
2026-01-06 14:17:04 +08:00
|
|
|
|
let item = singleLayers[index];
|
2026-01-02 11:24:11 +08:00
|
|
|
|
let id = generateId("layer_image_");
|
2026-01-06 14:17:04 +08:00
|
|
|
|
let name = item.name;
|
2026-01-02 11:24:11 +08:00
|
|
|
|
let image = await new Promise(resolve => {
|
2026-01-06 14:17:04 +08:00
|
|
|
|
fabric.Image.fromURL(item.path, (fabricImage)=>{
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const imgElement = fabricImage.getElement();
|
|
|
|
|
|
const tcanvas = document.createElement('canvas');
|
|
|
|
|
|
tcanvas.width = imgElement.width;
|
|
|
|
|
|
tcanvas.height = imgElement.height;
|
|
|
|
|
|
const ctx = tcanvas.getContext('2d');
|
|
|
|
|
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
|
|
|
|
|
ctx.drawImage(imgElement, 0, 0);
|
|
|
|
|
|
resolve(tcanvas);
|
|
|
|
|
|
}, { crossOrigin: "anonymous" });
|
|
|
|
|
|
})
|
2026-01-16 10:29:03 +08:00
|
|
|
|
let scaleX_ = fixedLayerObj.width / image.width * (item.scale?.[0] || 1) / 5;
|
|
|
|
|
|
let scaleY_ = fixedLayerObj.height / image.height * (item.scale?.[1] || 1) / 5;
|
|
|
|
|
|
let scale = fixedLayerObj.width > fixedLayerObj.height ? scaleX_ : scaleY_;
|
|
|
|
|
|
let offsetX = (item.location?.[0] || 0) - image.width * scale / 2
|
|
|
|
|
|
let offsetY = (item.location?.[1] || 0) - image.height * scale / 2
|
|
|
|
|
|
let top = fixedLayerObj.top - fixedLayerObj.height * fixedLayerObj.scaleY / 2
|
|
|
|
|
|
let left = fixedLayerObj.left - fixedLayerObj.width * fixedLayerObj.scaleX / 2
|
|
|
|
|
|
let scaleX = fixedLayerObj.scaleX
|
|
|
|
|
|
let scaleY = fixedLayerObj.scaleY
|
|
|
|
|
|
let opacity = 1
|
|
|
|
|
|
let angle = 0
|
|
|
|
|
|
let gapX = 0
|
|
|
|
|
|
let gapY = 0
|
|
|
|
|
|
let fillSource = image
|
|
|
|
|
|
if(item.object){
|
|
|
|
|
|
top += item.object.top * fixedLayerObj.scaleY
|
|
|
|
|
|
left += item.object.left * fixedLayerObj.scaleX
|
|
|
|
|
|
scaleX *= item.object.scaleX
|
|
|
|
|
|
scaleY *= item.object.scaleY
|
|
|
|
|
|
opacity = item.object.opacity
|
|
|
|
|
|
angle = item.object.angle
|
|
|
|
|
|
gapX = item.object.gapX
|
|
|
|
|
|
gapY = item.object.gapY
|
|
|
|
|
|
fillSource = imageAddGapToCanvas(image, gapX, gapY);
|
|
|
|
|
|
}
|
2026-01-02 11:24:11 +08:00
|
|
|
|
let rect = new fabric.Rect({
|
|
|
|
|
|
id: id,
|
|
|
|
|
|
layerId: id,
|
|
|
|
|
|
layerName: name,
|
|
|
|
|
|
width: fixedLayerObj.width,
|
|
|
|
|
|
height: fixedLayerObj.height,
|
2026-01-16 10:29:03 +08:00
|
|
|
|
top: top,
|
|
|
|
|
|
left: left,
|
|
|
|
|
|
scaleX: scaleX,
|
|
|
|
|
|
scaleY: scaleY,
|
2026-01-08 14:29:10 +08:00
|
|
|
|
globalCompositeOperation: BlendMode.MULTIPLY,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
fill: new fabric.Pattern({
|
2026-01-16 10:29:03 +08:00
|
|
|
|
source: fillSource,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
repeat: "repeat",
|
2026-01-06 14:17:04 +08:00
|
|
|
|
patternTransform: createPatternTransform(scale, item.angle || 0),
|
2026-01-16 10:29:03 +08:00
|
|
|
|
offsetX: offsetX, // 水平偏移
|
|
|
|
|
|
offsetY: offsetY, // 垂直偏移
|
2026-01-02 11:24:11 +08:00
|
|
|
|
}),
|
2026-01-06 14:17:04 +08:00
|
|
|
|
fill_ : {
|
|
|
|
|
|
source: item.path,
|
2026-01-16 10:29:03 +08:00
|
|
|
|
gapX: gapX,
|
|
|
|
|
|
gapY: gapY,
|
2026-01-06 14:17:04 +08:00
|
|
|
|
width: image.width,
|
|
|
|
|
|
height: image.height,
|
2026-01-08 14:29:10 +08:00
|
|
|
|
},
|
2026-01-14 11:26:51 +08:00
|
|
|
|
isPrintTrims: true,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
this.canvas.add(rect);
|
|
|
|
|
|
let layer = createLayer({
|
|
|
|
|
|
id: id,
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
type: LayerType.BITMAP,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
locked: true,
|
|
|
|
|
|
opacity: 1,
|
2026-01-14 11:26:51 +08:00
|
|
|
|
isPrintTrims: true,
|
2026-01-08 14:29:10 +08:00
|
|
|
|
blendMode: BlendMode.MULTIPLY,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
fabricObjects: [rect.toObject(["id", "layerId", "layerName"])],
|
2026-01-08 14:29:10 +08:00
|
|
|
|
metadata: {sourceData: item},
|
2026-01-02 11:24:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
children.push(layer);
|
|
|
|
|
|
};
|
2026-01-08 15:25:15 +08:00
|
|
|
|
// if(children.length === 0){
|
|
|
|
|
|
// let layer = createLayer({
|
|
|
|
|
|
// id: generateId("layer_image_"),
|
|
|
|
|
|
// name: t("Canvas.EmptyLayer"),
|
|
|
|
|
|
// type: LayerType.BITMAP,
|
|
|
|
|
|
// visible: true,
|
|
|
|
|
|
// locked: false,
|
|
|
|
|
|
// opacity: 1.0,
|
|
|
|
|
|
// fabricObjects: [],
|
|
|
|
|
|
// })
|
|
|
|
|
|
// children.push(layer);
|
|
|
|
|
|
// }
|
2026-01-14 11:26:51 +08:00
|
|
|
|
if(children.length === 0) return;
|
2026-01-02 11:24:11 +08:00
|
|
|
|
const groupRect = new fabric.Rect({});
|
2026-01-05 11:47:36 +08:00
|
|
|
|
await this.setObjecCliptInfo(groupRect);
|
2026-01-02 11:24:11 +08:00
|
|
|
|
// 插入组图层
|
|
|
|
|
|
const groupIndex = this.layers.value.findIndex(layer => layer.isFixedOther || layer.isFixed || layer.isBackground);
|
|
|
|
|
|
const groupLayer = createLayer({
|
|
|
|
|
|
id: SpecialLayerId.SPECIAL_GROUP,
|
|
|
|
|
|
name: t("Canvas.PrintAndElementsGroup"),
|
|
|
|
|
|
type: LayerType.GROUP,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
locked: false,
|
|
|
|
|
|
opacity: 1.0,
|
|
|
|
|
|
fabricObjects: [],
|
|
|
|
|
|
children: children,
|
|
|
|
|
|
clippingMask: groupRect.toObject(),
|
2026-01-12 09:42:07 +08:00
|
|
|
|
isPrintTrimsGroup: true,
|
2026-01-02 11:24:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
this.layers.value.splice(groupIndex, 0, groupLayer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 11:47:36 +08:00
|
|
|
|
/**
|
2026-01-06 14:17:04 +08:00
|
|
|
|
* 画布事件变更后
|
2026-01-05 11:47:36 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async changeCanvas(){
|
2026-01-08 14:29:10 +08:00
|
|
|
|
const fixedLayerObj = this.getFixedLayerObject();
|
|
|
|
|
|
if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj)
|
|
|
|
|
|
const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
|
|
|
|
|
|
if(colorObject){
|
|
|
|
|
|
const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP);
|
|
|
|
|
|
if(ids.length === 0){
|
|
|
|
|
|
ids.unshift(SpecialLayerId.SPECIAL_GROUP);
|
|
|
|
|
|
await this.setObjecCliptInfo(colorObject);
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true});
|
|
|
|
|
|
if(!base64) return console.warn("导出图片失败", base64)
|
|
|
|
|
|
const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true);
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
const width = fixedLayerObj.width;
|
|
|
|
|
|
const height = fixedLayerObj.height;
|
|
|
|
|
|
const x = (canvas.width - width) / 2;
|
|
|
|
|
|
const y = (canvas.height - height) / 2;
|
|
|
|
|
|
const data = ctx.getImageData(x, y, width, height);
|
|
|
|
|
|
await this.setObjecCliptInfo(colorObject, data);
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
}
|
2026-01-05 11:47:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 缩放红绿图模式内容以适应当前画布大小
|
|
|
|
|
|
* 确保衣服底图和红绿图永远在画布内可见
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_rescaleRedGreenModeContent() {
|
|
|
|
|
|
if (!this.canvas) return;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("正在重新缩放红绿图内容...");
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取固定图层和普通图层
|
|
|
|
|
|
const fixedLayerObject = this._getFixedLayerObject();
|
|
|
|
|
|
const normalLayerObjects = this._getNormalLayerObjects();
|
|
|
|
|
|
|
|
|
|
|
|
if (!fixedLayerObject) {
|
|
|
|
|
|
console.warn("找不到固定图层对象,无法进行红绿图内容缩放");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算边距(画布两侧各留出一定空间)
|
|
|
|
|
|
const margin = 50;
|
|
|
|
|
|
const maxWidth = this.canvas.width - margin * 2;
|
|
|
|
|
|
const maxHeight = this.canvas.height - margin * 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算原始尺寸
|
|
|
|
|
|
const originalWidth = fixedLayerObject.width * fixedLayerObject.scaleX;
|
|
|
|
|
|
const originalHeight = fixedLayerObject.height * fixedLayerObject.scaleY;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算需要的缩放比例,确保图像完全适应画布
|
|
|
|
|
|
const scaleX = maxWidth / originalWidth;
|
|
|
|
|
|
const scaleY = maxHeight / originalHeight;
|
|
|
|
|
|
const scale = Math.min(scaleX, scaleY);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`计算的缩放比例: ${scale},原始尺寸: ${originalWidth}x${originalHeight},目标尺寸: ${maxWidth}x${maxHeight}`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果缩放比例接近1,不进行缩放
|
|
|
|
|
|
if (Math.abs(scale - 1) < 0.05) {
|
|
|
|
|
|
console.log("缩放比例接近1,不进行缩放,仅居中内容");
|
|
|
|
|
|
this.centerAllObjects();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 缩放固定图层(衣服底图)
|
|
|
|
|
|
this._rescaleObject(fixedLayerObject, scale);
|
|
|
|
|
|
|
|
|
|
|
|
// 缩放所有普通图层对象(红绿图和其他内容)
|
|
|
|
|
|
normalLayerObjects.forEach((obj) => {
|
|
|
|
|
|
// 红绿图对象应与底图保持完全一致的位置和大小
|
|
|
|
|
|
if (this._isLikelyRedGreenImage(obj, fixedLayerObject)) {
|
|
|
|
|
|
// 完全匹配底图的位置和大小
|
|
|
|
|
|
obj.set({
|
|
|
|
|
|
scaleX: fixedLayerObject.scaleX,
|
|
|
|
|
|
scaleY: fixedLayerObject.scaleY,
|
|
|
|
|
|
left: fixedLayerObject.left,
|
|
|
|
|
|
top: fixedLayerObject.top,
|
|
|
|
|
|
originX: fixedLayerObject.originX || "center",
|
|
|
|
|
|
originY: fixedLayerObject.originY || "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他普通对象进行等比例缩放
|
|
|
|
|
|
this._rescaleObject(obj, scale);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 重新居中所有内容
|
|
|
|
|
|
this.centerAllObjects();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新所有对象的坐标系统
|
|
|
|
|
|
this.canvas.getObjects().forEach((obj) => {
|
|
|
|
|
|
obj.setCoords();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染画布
|
|
|
|
|
|
this.canvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
console.log("红绿图内容缩放完成");
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("缩放红绿图内容时出错:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 缩放单个对象
|
|
|
|
|
|
* @param {Object} obj fabric对象
|
|
|
|
|
|
* @param {Number} scale 缩放比例
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_rescaleObject(obj, scale) {
|
|
|
|
|
|
if (!obj) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 保存原始中心点
|
|
|
|
|
|
const center = obj.getCenterPoint();
|
|
|
|
|
|
|
|
|
|
|
|
// 应用新的缩放
|
|
|
|
|
|
obj.set({
|
|
|
|
|
|
scaleX: obj.scaleX * scale,
|
|
|
|
|
|
scaleY: obj.scaleY * scale,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 重新定位到原中心点
|
|
|
|
|
|
obj.setPositionByOrigin(center, "center", "center");
|
|
|
|
|
|
obj.setCoords();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取固定图层对象(衣服底图)
|
|
|
|
|
|
* @returns {Object|null} 固定图层对象或null
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_getFixedLayerObject() {
|
|
|
|
|
|
if (!this.layers || !this.layers.value) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// 查找固定图层
|
|
|
|
|
|
const fixedLayer = this.layers.value.find((layer) => layer.isFixed);
|
|
|
|
|
|
if (!fixedLayer) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// 返回图层中的fabric对象
|
|
|
|
|
|
return fixedLayer.fabricObject || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-02 11:24:11 +08:00
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有普通图层对象(包括红绿图)
|
|
|
|
|
|
* @returns {Array} 普通图层对象数组
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_getNormalLayerObjects() {
|
|
|
|
|
|
if (!this.layers || !this.layers.value) return [];
|
|
|
|
|
|
|
|
|
|
|
|
// 查找所有非背景、非固定的普通图层
|
2025-07-21 01:17:25 +08:00
|
|
|
|
const normalLayers = this.layers.value.filter(
|
|
|
|
|
|
(layer) => !layer.isBackground && !layer.isFixed
|
|
|
|
|
|
);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 收集所有普通图层中的对象
|
|
|
|
|
|
const objects = [];
|
|
|
|
|
|
normalLayers.forEach((layer) => {
|
|
|
|
|
|
// 如果有单个对象属性
|
|
|
|
|
|
if (layer.fabricObject) {
|
|
|
|
|
|
objects.push(layer.fabricObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有对象数组
|
|
|
|
|
|
if (Array.isArray(layer.fabricObjects)) {
|
|
|
|
|
|
layer.fabricObjects.forEach((obj) => {
|
|
|
|
|
|
if (obj) objects.push(obj);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return objects;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 判断对象是否可能是红绿图
|
|
|
|
|
|
* 通过比较与衣服底图的大小、位置来判断
|
|
|
|
|
|
* @param {Object} obj 要检查的对象
|
|
|
|
|
|
* @param {Object} fixedLayerObject 固定图层对象(衣服底图)
|
|
|
|
|
|
* @returns {Boolean} 是否可能是红绿图
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
_isLikelyRedGreenImage(obj, fixedLayerObject) {
|
|
|
|
|
|
if (!obj || !fixedLayerObject) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查对象是否为图像
|
|
|
|
|
|
if (obj.type !== "image") return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 比较尺寸(允许5%的误差)
|
|
|
|
|
|
const sizeMatch =
|
2025-07-21 01:17:25 +08:00
|
|
|
|
Math.abs(
|
|
|
|
|
|
obj.width * obj.scaleX -
|
|
|
|
|
|
fixedLayerObject.width * fixedLayerObject.scaleX
|
|
|
|
|
|
) <
|
2025-06-09 10:25:54 +08:00
|
|
|
|
fixedLayerObject.width * fixedLayerObject.scaleX * 0.05 &&
|
2025-07-21 01:17:25 +08:00
|
|
|
|
Math.abs(
|
|
|
|
|
|
obj.height * obj.scaleY -
|
|
|
|
|
|
fixedLayerObject.height * fixedLayerObject.scaleY
|
|
|
|
|
|
) <
|
2025-06-09 10:25:54 +08:00
|
|
|
|
fixedLayerObject.height * fixedLayerObject.scaleY * 0.05;
|
|
|
|
|
|
|
|
|
|
|
|
// 比较位置(允许一定的偏差)
|
|
|
|
|
|
const positionMatch =
|
|
|
|
|
|
Math.abs(obj.left - fixedLayerObject.left) < 50 &&
|
|
|
|
|
|
Math.abs(obj.top - fixedLayerObject.top) < 50;
|
|
|
|
|
|
|
|
|
|
|
|
return sizeMatch && positionMatch;
|
|
|
|
|
|
}
|
2026-01-02 11:24:11 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 键盘移动激活对象
|
|
|
|
|
|
* @param {String} direction 移动方向(up, down, left, right)
|
|
|
|
|
|
* @param {<Number>} step 移动步长
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
moveActiveObject(direction, step = 1) {
|
|
|
|
|
|
const objects = [];
|
|
|
|
|
|
const activeObject = this.canvas.getActiveObject();
|
|
|
|
|
|
if(!activeObject) return;
|
|
|
|
|
|
const initPos = {
|
|
|
|
|
|
id: activeObject.id,
|
|
|
|
|
|
left: activeObject.left,
|
|
|
|
|
|
top: activeObject.top,
|
|
|
|
|
|
};
|
|
|
|
|
|
switch(direction) {
|
|
|
|
|
|
case "up":
|
|
|
|
|
|
activeObject.top -= step;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "down":
|
|
|
|
|
|
activeObject.top += step;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "left":
|
|
|
|
|
|
activeObject.left -= step;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "right":
|
|
|
|
|
|
activeObject.left += step;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(!activeObject.id) return this.canvas.renderAll();
|
|
|
|
|
|
const cmd = new ObjectMoveCommand({
|
|
|
|
|
|
canvas: this.canvas,
|
|
|
|
|
|
initPos,
|
|
|
|
|
|
finalPos: {
|
|
|
|
|
|
id: activeObject.id,
|
|
|
|
|
|
left: activeObject.left,
|
|
|
|
|
|
top: activeObject.top,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
this.commandManager.executeCommand(cmd);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|