合并画布代码

This commit is contained in:
X1627315083
2025-06-18 11:05:23 +08:00
parent 903c0ebdf5
commit 9c7fae36eb
118 changed files with 23633 additions and 8201 deletions

View File

@@ -1,5 +1,5 @@
import { Command } from "./Command";
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
/**
* 创建背景图层命令
@@ -195,10 +195,13 @@ export class BackgroundSizeCommand extends Command {
this.newWidth = options.newWidth;
this.newHeight = options.newHeight;
this.historyManager = options.historyManager;
this.isRedGreenMode = options.isRedGreenMode;
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
// 记录原尺寸
this.oldWidth = this.canvas.width;
this.oldHeight = this.canvas.height;
this.oldWidth = this.bgLayer.fabricObject.width;
this.oldHeight = this.bgLayer.fabricObject.height;
// 查找背景图层
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);

View File

@@ -609,12 +609,15 @@ export class TextureUploadCommand extends BaseBrushCommand {
constructor(options = {}) {
super({
...options,
name: `上传纹理: ${options.name || options.file?.name || '未知'}`,
name: `上传纹理: ${options.name || options.file?.name || "未知"}`,
description: `上传自定义纹理文件`,
});
this.file = options.file;
this.name = options.name || options.file?.name?.replace(/\.[^/.]+$/, "") || "自定义纹理";
this.name =
options.name ||
options.file?.name?.replace(/\.[^/.]+$/, "") ||
"自定义纹理";
this.category = options.category || "自定义材质";
this.texturePresetManager = options.texturePresetManager;
this.brushManager = options.brushManager;
@@ -623,13 +626,13 @@ export class TextureUploadCommand extends BaseBrushCommand {
async execute() {
if (!this.file || !this.texturePresetManager) {
throw new Error('缺少必要的文件或纹理预设管理器');
throw new Error("缺少必要的文件或纹理预设管理器");
}
try {
// 创建文件 data URL
const dataUrl = await this._fileToDataUrl(this.file);
// 添加到纹理预设管理器
this.uploadedTextureId = this.texturePresetManager.addCustomTexture({
name: this.name,
@@ -654,10 +657,10 @@ export class TextureUploadCommand extends BaseBrushCommand {
return {
textureId: this.uploadedTextureId,
dataUrl: dataUrl,
name: this.name
name: this.name,
};
} catch (error) {
console.error('纹理上传失败:', error);
console.error("纹理上传失败:", error);
throw new Error(`纹理上传失败: ${error.message}`);
}
}
@@ -672,7 +675,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
this.texturePresetManager.removeCustomTexture(this.uploadedTextureId);
return true;
} catch (error) {
console.error('撤销纹理上传失败:', error);
console.error("撤销纹理上传失败:", error);
return false;
}
}
@@ -680,7 +683,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
/**
* 将文件转换为 data URL
* @private
* @param {File} file
* @param {File} file
* @returns {Promise<string>}
*/
_fileToDataUrl(file) {

View File

@@ -0,0 +1,100 @@
import { isArray } from "lodash-es";
import { optimizeCanvasRendering } from "../utils/helper";
import { restoreObjectLayerAssociations } from "../utils/layerUtils";
import { Command } from "./Command";
/**
* 橡皮擦操作命令
* 支持橡皮擦操作的撤销和重做
*/
export class EraserCommand extends Command {
/**
* 构造函数
* @param {Object} options 命令选项
* @param {Object} options.canvas Fabric画布实例
* @param {Object} options.layerManager 图层管理器
* @param {Object} options.beforeSnapshot 擦除前的状态快照
* @param {Object} options.afterSnapshot 擦除后的状态快照
* @param {Array} options.affectedObjects 擦除影响的对象列表(可选)
*/
constructor(options) {
super({
name: "橡皮擦操作",
description: `擦除`,
...options,
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.beforeSnapshot = options.beforeSnapshot;
this.afterSnapshot = options.afterSnapshot;
this.affectedObjects = options.affectedObjects || [];
this.fristLoad = true; // 是否是第一次加载
}
/**
* 执行橡皮擦操作
*/
async execute() {
if (!this.beforeSnapshot || !this.afterSnapshot) {
console.warn("缺少状态快照,无法执行橡皮擦命令");
return false;
}
if (!this.fristLoad)
await this._restoreCanvasState(this.afterSnapshot); // 应用重做的状态
else await this.layerManager?.updateLayersObjectsInteractivity?.(false);
this.fristLoad = false; // 标记为非第一次加载
return true;
}
/**
* 撤销橡皮擦操作
*/
async undo() {
if (!this.beforeSnapshot) {
console.warn("缺少擦除前的状态快照,无法撤销");
return false;
}
// 恢复到擦除前的状态
await this._restoreCanvasState(this.beforeSnapshot);
return true;
}
/**
* 恢复画布状态
* @param {Object} snapshot 状态快照
* @private
*/
async _restoreCanvasState(snapshot) {
// 对比 eraser erasable 两个属性如果当前对象的eraser属性和erasable属性不一致则需要更新对象的eraser属性
if (!snapshot || !snapshot.objects) return;
// 优化渲染 - 统一批处理 支持异步回调 防止闪屏
await optimizeCanvasRendering(this.canvas, async () => {
return new Promise((resolve) => {
this.canvas.loadFromJSON(snapshot, async () => {
// 恢复图层关联
this._restoreObjectLayerAssociations();
// 确保所有对象的交互性正确设置
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
resolve();
});
});
});
}
/**
* 恢复对象与图层的关联关系
* @private
*/
_restoreObjectLayerAssociations() {
if (!this.layerManager) return;
const canvasObjects = this.canvas.getObjects();
restoreObjectLayerAssociations(
this.layerManager?.layers?.value,
canvasObjects
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js";
import { ToolCommand } from "./ToolCommands.js";
import { ClearSelectionCommand } from "./SelectionCommands.js";
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
/**
* 套索抠图命令

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import { Command, FunctionCommand } from "./Command";
import { getLiquifyReferenceManager } from "../managers/LiquifyReferenceManager";
/**
* 液化命令基类
@@ -192,81 +193,6 @@ export class LiquifyCommand extends Command {
}
}
/**
* 图层栅格化命令
* 用于将复杂图层栅格化为单一图像,以便进行液化操作
*/
export class RasterizeForLiquifyCommand extends Command {
/**
* 创建栅格化命令
* @param {Object} options 配置选项
* @param {Object} options.canvas Fabric.js画布实例
* @param {Object} options.layerManager 图层管理器实例
* @param {String} options.layerId 需要栅格化的图层ID
*/
constructor(options) {
super({
name: options.name || "栅格化图层",
description: options.description || "将图层栅格化为单一图像以便液化操作",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.originalLayer = null;
this.rasterizedImageObj = null;
}
/**
* 执行栅格化操作
* @returns {Promise<Object>} 栅格化后的图像对象
*/
async execute() {
if (!this.canvas || !this.layerManager || !this.layerId) {
throw new Error("栅格化命令缺少必要参数");
}
// 保存原始图层信息
this.originalLayer = this.layerManager.getLayerById(this.layerId);
if (!this.originalLayer) {
throw new Error(`图层ID不存在: ${this.layerId}`);
}
// 栅格化图层
const rasterizedImage = await this.layerManager.rasterizeLayer(
this.layerId
);
if (!rasterizedImage) {
throw new Error("栅格化图层失败");
}
this.rasterizedImageObj = rasterizedImage;
return rasterizedImage;
}
/**
* 撤销栅格化操作
* 注意:完整撤销栅格化是复杂的,这里提供近似还原
* @returns {Promise<boolean>} 撤销结果
*/
async undo() {
if (!this.canvas || !this.layerManager || !this.originalLayer) {
throw new Error("无法撤销:缺少必要的状态信息");
}
// 恢复图层为原始状态是复杂的这里可能需要与LayerManager协作
// 这个实现可能需要根据实际的LayerManager功能来调整
const restored = await this.layerManager.restoreLayerFromBackup(
this.layerId
);
if (!restored) {
console.warn("无法完全还原栅格化前的图层状态");
}
return true;
}
}
/**
* 液化工具初始化命令
* 用于初始化液化工具的状态,不执行实际操作
@@ -550,6 +476,405 @@ export class LiquifyResetCommand extends LiquifyCommand {
}
}
/**
* 液化状态命令 - 最优化版本
* 使用引用管理器避免对象引用丢失,支持高性能的状态管理
*/
export class LiquifyStateCommand extends Command {
/**
* 创建液化状态命令
* @param {Object} options 配置选项
* @param {Object} options.canvas Fabric.js画布实例
* @param {Object} options.layerManager 图层管理器实例
* @param {Object} options.targetObject 目标对象(保持引用)
* @param {String} options.targetLayerId 目标图层ID
* @param {ImageData} options.initialImageData 初始图像数据
* @param {ImageData} options.finalImageData 最终图像数据
*/
constructor(options) {
super({
name: options.name || "液化操作",
description: options.description || "液化变形操作的状态记录",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.targetObject = options.targetObject;
this.targetLayerId = options.targetLayerId;
this.targetObjectId = options.targetObjectId;
// 获取引用管理器实例
this.refManager = getLiquifyReferenceManager();
// 注册对象到引用管理器
this.objectRefId = this.refManager.registerObject(
this.targetObject,
this.targetObjectId || `liquify_${Date.now()}`
);
// 保存状态快照ID
this.initialSnapshotId = null;
this.finalSnapshotId = null;
// 设置图像数据
this.initialImageData = options.initialImageData;
this.finalImageData = options.finalImageData;
this.currentState = "initial";
// 创建初始快照
if (this.initialImageData) {
this._createInitialSnapshot();
}
}
/**
* 执行命令 - 应用最终状态
*/
async execute() {
if (!this.finalImageData) {
throw new Error("缺少最终状态数据");
}
// 确保有最终状态快照
if (!this.finalSnapshotId) {
await this._createFinalSnapshot();
}
// 通过引用管理器更新图像数据
await this.refManager.updateObjectImageData(
this.objectRefId,
this.finalImageData
);
this.currentState = "final";
this.canvas.renderAll();
console.log("✅ 液化命令执行完成,应用最终状态");
return true;
}
/**
* 撤销命令 - 恢复初始状态
*/
async undo() {
if (!this.initialImageData || !this.initialSnapshotId) {
throw new Error("缺少初始状态数据或快照");
}
// 通过引用管理器恢复到初始快照
await this.refManager.restoreFromSnapshot(
this.objectRefId,
this.initialSnapshotId
);
this.currentState = "initial";
this.canvas.renderAll();
console.log("🔄 液化命令撤销完成,恢复初始状态");
return true;
}
/**
* 重做命令 - 重新应用最终状态
*/
async redo() {
return this.execute();
}
/**
* 设置最终状态的图像数据
* @param {ImageData} finalImageData 最终状态的图像数据
*/
setFinalImageData(finalImageData) {
this.finalImageData = finalImageData;
this.finalSnapshotId = null; // 重置快照ID下次执行时重新创建
}
/**
* 获取当前状态标记
* @returns {String} 'initial' 或 'final'
*/
getCurrentState() {
return this.currentState;
}
/**
* 检查命令是否有效
* @returns {Boolean} 是否有效
*/
isValid() {
const targetObject = this.refManager.getObjectRef(this.objectRefId);
return !!(targetObject && this.initialImageData && this.finalImageData);
}
/**
* 获取目标对象的当前引用
* @returns {Object|null} Fabric对象
*/
getTargetObject() {
return this.refManager.getObjectRef(this.objectRefId);
}
/**
* 清理资源
*/
dispose() {
if (this.objectRefId) {
// 清理快照
if (this.initialSnapshotId) {
this.refManager.stateSnapshots.delete(this.initialSnapshotId);
}
if (this.finalSnapshotId) {
this.refManager.stateSnapshots.delete(this.finalSnapshotId);
}
// 注意:不要清理对象引用,因为可能有其他命令在使用
console.log(`🗑️ 液化状态命令资源已清理: ${this.objectRefId}`);
}
}
/**
* 获取内存使用统计
* @returns {Object} 统计信息
*/
getMemoryStats() {
const refManagerStats = this.refManager.getMemoryStats();
const commandMemory = this._calculateCommandMemory();
return {
...refManagerStats,
commandMemory,
totalCommandMemory: commandMemory,
};
}
// 私有方法
/**
* 创建初始状态快照
* @private
*/
async _createInitialSnapshot() {
if (!this.initialImageData) return;
this.initialSnapshotId = `${this.objectRefId}_initial_${Date.now()}`;
// 手动创建初始快照,包含图像数据
const fabricObject = this.refManager.getObjectRef(this.objectRefId);
if (fabricObject) {
const snapshot = {
timestamp: Date.now(),
properties: this.refManager._captureObjectState(fabricObject),
imageData: this.initialImageData,
eventListeners:
this.refManager.eventListeners.get(this.objectRefId) || {},
};
this.refManager.stateSnapshots.set(this.initialSnapshotId, snapshot);
console.log(`📸 初始状态快照已创建: ${this.initialSnapshotId}`);
}
}
/**
* 创建最终状态快照
* @private
*/
async _createFinalSnapshot() {
if (!this.finalImageData) return;
this.finalSnapshotId = `${this.objectRefId}_final_${Date.now()}`;
// 手动创建最终快照,包含图像数据
const fabricObject = this.refManager.getObjectRef(this.objectRefId);
if (fabricObject) {
const snapshot = {
timestamp: Date.now(),
properties: this.refManager._captureObjectState(fabricObject),
imageData: this.finalImageData,
eventListeners:
this.refManager.eventListeners.get(this.objectRefId) || {},
};
this.refManager.stateSnapshots.set(this.finalSnapshotId, snapshot);
console.log(`📸 最终状态快照已创建: ${this.finalSnapshotId}`);
}
}
/**
* 计算命令本身的内存使用量
* @returns {Number} 内存使用量(字节)
* @private
*/
_calculateCommandMemory() {
let bytes = 0;
// 计算ImageData内存使用
if (this.initialImageData) {
bytes += this.initialImageData.width * this.initialImageData.height * 4;
}
if (this.finalImageData) {
bytes += this.finalImageData.width * this.finalImageData.height * 4;
}
return bytes;
}
}
/**
* 批量液化状态命令 - 用于处理多个对象的液化操作
*/
export class BatchLiquifyStateCommand extends Command {
/**
* 创建批量液化状态命令
* @param {Object} options 配置选项
* @param {Array} options.commands 液化状态命令列表
* @param {Object} options.canvas Fabric.js画布实例
*/
constructor(options) {
super({
name: options.name || "批量液化操作",
description:
options.description || `批量液化${options.commands?.length || 0}个对象`,
});
this.commands = options.commands || [];
this.canvas = options.canvas;
this.refManager = getLiquifyReferenceManager();
}
/**
* 添加液化状态命令
* @param {LiquifyStateCommand} command 液化状态命令
*/
addCommand(command) {
this.commands.push(command);
}
/**
* 执行批量操作
*/
async execute() {
const results = [];
const updates = [];
// 准备批量更新数据
for (const command of this.commands) {
if (command.finalImageData && command.objectRefId) {
updates.push({
refId: command.objectRefId,
imageData: command.finalImageData,
});
}
}
// 执行批量更新
if (updates.length > 0) {
const updateResults = await this.refManager.batchUpdate(updates);
results.push(...updateResults);
}
// 更新命令状态
this.commands.forEach((command) => {
command.currentState = "final";
});
this.canvas.renderAll();
return results;
}
/**
* 撤销批量操作
*/
async undo() {
// 逆序撤销
for (let i = this.commands.length - 1; i >= 0; i--) {
await this.commands[i].undo();
}
this.canvas.renderAll();
return true;
}
/**
* 重做批量操作
*/
async redo() {
return this.execute();
}
/**
* 检查批量命令是否有效
* @returns {Boolean} 是否有效
*/
isValid() {
return (
this.commands.length > 0 && this.commands.every((cmd) => cmd.isValid())
);
}
/**
* 清理所有子命令的资源
*/
dispose() {
this.commands.forEach((command) => {
if (command.dispose) {
command.dispose();
}
});
}
}
/**
* 序列化fabric对象为JSON
* @param {Object} fabricObject fabric对象
* @returns {Object} 序列化后的对象状态
*/
export function serializeFabricObject(fabricObject) {
if (!fabricObject) {
throw new Error("无法序列化:对象为空");
}
try {
// 使用fabric.js的toObject方法序列化
const serializedObject = fabricObject.toObject([
"id",
"objectId",
"uid",
"layerId",
"name",
"type",
]);
// 记录额外的元数据
const metadata = {
timestamp: Date.now(),
objectType: fabricObject.type,
objectId: fabricObject.id || fabricObject.objectId || fabricObject.uid,
layerId: fabricObject.layerId,
bounds: fabricObject.getBoundingRect(),
};
return {
serializedObject,
metadata,
version: "1.0",
};
} catch (error) {
console.error("序列化fabric对象失败:", error);
throw error;
}
}
/**
* 辅助函数:创建液化状态命令
* @param {Object} options 配置选项
* @returns {LiquifyStateCommand} 状态命令实例
*/
export function createLiquifyStateCommand(options) {
return new LiquifyStateCommand(options);
}
/**
* 辅助函数:创建液化重置命令
* @param {Object} options 配置选项

View File

@@ -1,6 +1,11 @@
import { OperationType } from "../utils/layerHelper.js";
import {
findObjectById,
generateId,
optimizeCanvasRendering,
} from "../utils/helper.js";
import { LayerType, OperationType } from "../utils/layerHelper.js";
import { Command, CompositeCommand } from "./Command.js";
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
/**
* 批量初始化红绿图模式命令
@@ -29,100 +34,134 @@ export class BatchInitializeRedGreenModeCommand extends Command {
this.originalNormalObjects = null;
this.originalNormalOpacities = new Map();
this.originalToolState = null;
this.originalActiveLayerId = null;
// 存储加载的图片对象
this.clothingImage = null;
this.redGreenImage = null;
// 存储新创建的图层ID
this.newEmptyLayerId = null;
}
async execute() {
try {
// 禁用画布渲染以避免闪烁
this.canvas.renderOnAddRemove = false;
await optimizeCanvasRendering(this.canvas, async () => {
// 1. 设置画布背景为白色
this.originalCanvasBackground = this.canvas.backgroundColor;
this.canvas.setBackgroundColor("#ffffff", () => {});
// 1. 设置画布背景为白色
this.originalCanvasBackground = this.canvas.backgroundColor;
this.canvas.setBackgroundColor('#ffffff', () => {});
// 2. 查找图层结构
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
// 2. 查找图层结构
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
throw new Error("缺少必要的图层结构");
}
const normalLayer = normalLayers[0]; // 使用第一个普通图层
// 3. 保存原始状态
this.originalBackgroundObject = backgroundLayer.fabricObject ? {
...backgroundLayer.fabricObject.toObject(),
ref: backgroundLayer.fabricObject
} : null;
this.originalFixedObjects = fixedLayer.fabricObject
? [fixedLayer.fabricObject]
: [];
this.originalNormalObjects = normalLayer.fabricObjects
? [...normalLayer.fabricObjects]
: [];
// 保存普通图层透明度
normalLayers.forEach((layer) => {
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
this.originalNormalOpacities.set(
`${layer.id}_${obj.id || "unknown"}`,
obj.opacity || 1
);
});
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
throw new Error("缺少必要的图层结构");
}
});
// 保存工具状态
if (this.toolManager) {
this.originalToolState = {
currentTool: this.toolManager.getCurrentTool(),
isRedGreenMode: this.toolManager.isRedGreenMode,
};
}
const normalLayer = normalLayers[0]; // 使用第一个普通图层
// 4. 确保背景图层大小正确
await this._setupBackgroundLayer(backgroundLayer);
// 3. 保存原始状态
this.originalBackgroundObject = backgroundLayer.fabricObject
? {
...backgroundLayer.fabricObject.toObject(),
ref: backgroundLayer.fabricObject,
}
: null;
// 5. 并行加载两个图片
const [clothingImg, redGreenImg] = await Promise.all([
this._loadImage(this.clothingImageUrl),
this._loadImage(this.redGreenImageUrl)
]);
this.originalFixedObjects = fixedLayer.fabricObject
? [fixedLayer.fabricObject]
: [];
// 6. 设置衣服底图到固定图层
await this._setupClothingImage(clothingImg, fixedLayer);
this.originalNormalObjects = normalLayer.fabricObjects
? [...normalLayer.fabricObjects]
: [];
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
await this._setupRedGreenImage(redGreenImg, normalLayer, this.clothingImage);
// 保存当前活动图层ID
this.originalActiveLayerId = this.layerManager.getActiveLayerId();
// 8. 设置普通图层透明度
this._setupNormalLayerOpacity(normalLayers);
// 保存普通图层透明度
normalLayers.forEach((layer) => {
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
this.originalNormalOpacities.set(
`${layer.id}_${obj.id || "unknown"}`,
obj.opacity || 1
);
});
}
});
// 9. 配置工具管理器
this._setupToolManager();
// 保存工具状态
if (this.toolManager) {
this.originalToolState = {
currentTool: this.toolManager.getCurrentTool(),
isRedGreenMode: this.toolManager.isRedGreenMode,
};
}
// 10. 重新启用渲染并执行一次性渲染
this.canvas.renderOnAddRemove = true;
this.canvas.renderAll();
// 5. 并行加载两个图片
const [clothingImg, redGreenImg] = await Promise.all([
this._loadImage(this.clothingImageUrl),
this._loadImage(this.redGreenImageUrl),
]);
console.log("批量红绿图模式初始化完成", {
衣服底图: this.clothingImageUrl,
红绿图: this.redGreenImageUrl,
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
画布背景: "白色",
// 6. 设置衣服底图到固定图层
await this._setupClothingImage(clothingImg, fixedLayer);
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
await this._setupRedGreenImage(
redGreenImg,
normalLayer,
this.clothingImage
);
// 4. 确保背景图层大小和衣服地图大小一致
await this._setupBackgroundLayer(backgroundLayer, this.clothingImage);
// 8. 设置普通图层透明度
this._setupNormalLayerOpacity(normalLayers); // 这里不需要在这里设置透明度 由图层统一处理
// 9. 创建新的空白图层并设置为活动图层
this.newEmptyLayerId = await this._createAndActivateEmptyLayer();
// 设置普通图层的裁剪对象为衣服底图
if (this.redGreenImage) {
// const clipPathImg = this.redGreenImage;
// clipPathImg.set({
// absolutePositioned: true,
// });
this.redGreenImage.set({
absolutePositioned: true,
});
const activeLayer = this.layerManager.getActiveLayer();
activeLayer.clippingMask = this.redGreenImage.toObject(["id"]);
activeLayer.opacity = this.normalLayerOpacity;
// activeLayer?.fabricObjects.forEach((obj) => {
// obj.set({
// clipPath: clipPathImg,
// });
// });
}
// 10. 配置工具管理器
this._setupToolManager();
console.log("批量红绿图模式初始化完成", {
衣服底图: this.clothingImageUrl,
红绿图: this.redGreenImageUrl,
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
画布背景: "白色",
新建空图层ID: this.newEmptyLayerId,
});
await this.layerManager.updateLayersObjectsInteractivity(false);
});
return true;
@@ -134,86 +173,125 @@ export class BatchInitializeRedGreenModeCommand extends Command {
}
}
/**
* 创建新的空白图层并设置为活动图层
* @returns {Promise<string>} 新创建的图层ID
* @private
*/
async _createAndActivateEmptyLayer() {
// 创建新的空白图层
const newLayerName = "绘制图层";
const newLayerId = this.layerManager.createLayer(
newLayerName,
LayerType.GROUP,
{
undoable: false,
}
);
// 设置为活动图层
if (newLayerId) {
this.layerManager.setActiveLayer(newLayerId);
}
return newLayerId;
}
async undo() {
try {
// 禁用渲染
this.canvas.renderOnAddRemove = false;
// 1. 恢复画布背景
if (this.originalCanvasBackground !== null) {
this.canvas.setBackgroundColor(this.originalCanvasBackground, () => {});
}
// 2. 恢复图层对象
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
// 移除当前添加的对象
if (this.clothingImage) {
this.canvas.remove(this.clothingImage);
}
if (this.redGreenImage) {
this.canvas.remove(this.redGreenImage);
}
// 恢复背景图层
if (backgroundLayer && this.originalBackgroundObject) {
if (this.originalBackgroundObject.ref) {
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
}
}
// 恢复固定图层
if (fixedLayer) {
fixedLayer.fabricObject = this.originalFixedObjects.length > 0
? this.originalFixedObjects[0]
: null;
if (fixedLayer.fabricObject) {
this.canvas.add(fixedLayer.fabricObject);
}
}
// 恢复普通图层
if (normalLayers.length > 0) {
const normalLayer = normalLayers[0];
normalLayer.fabricObjects = [...this.originalNormalObjects];
this.originalNormalObjects.forEach((obj) => {
this.canvas.add(obj);
});
}
// 3. 恢复透明度
normalLayers.forEach((layer) => {
if (this.originalNormalOpacities.has(layer.id)) {
layer.opacity = this.originalNormalOpacities.get(layer.id);
await optimizeCanvasRendering(this.canvas, async () => {
// 1. 恢复画布背景
if (this.originalCanvasBackground !== null) {
this.canvas.setBackgroundColor(
this.originalCanvasBackground,
() => {}
);
}
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
const key = `${layer.id}_${obj.id || "unknown"}`;
if (this.originalNormalOpacities.has(key)) {
obj.opacity = this.originalNormalOpacities.get(key);
}
// 2. 恢复图层对象
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
// 移除当前添加的对象
if (this.clothingImage) {
this.canvas.remove(this.clothingImage);
}
if (this.redGreenImage) {
this.canvas.remove(this.redGreenImage);
}
// 移除新创建的空白图层
if (this.newEmptyLayerId) {
const emptyLayerIndex = layers.findIndex(
(layer) => layer.id === this.newEmptyLayerId
);
if (emptyLayerIndex !== -1) {
layers.splice(emptyLayerIndex, 1);
}
}
// 恢复背景图层
if (backgroundLayer && this.originalBackgroundObject) {
if (this.originalBackgroundObject.ref) {
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
}
}
// 恢复固定图层
if (fixedLayer) {
fixedLayer.fabricObject =
this.originalFixedObjects.length > 0
? this.originalFixedObjects[0]
: null;
if (fixedLayer.fabricObject) {
this.canvas.add(fixedLayer.fabricObject);
}
}
// 恢复普通图层
if (normalLayers.length > 0) {
const normalLayer = normalLayers[0];
normalLayer.fabricObjects = [...this.originalNormalObjects];
this.originalNormalObjects.forEach((obj) => {
this.canvas.add(obj);
});
}
});
// 4. 恢复工具状态
if (this.toolManager && this.originalToolState) {
this.toolManager.isRedGreenMode = this.originalToolState.isRedGreenMode;
if (this.originalToolState.currentTool) {
this.toolManager.setTool(this.originalToolState.currentTool);
// 3. 恢复透明度
normalLayers.forEach((layer) => {
if (this.originalNormalOpacities.has(layer.id)) {
layer.opacity = this.originalNormalOpacities.get(layer.id);
}
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
const key = `${layer.id}_${obj.id || "unknown"}`;
if (this.originalNormalOpacities.has(key)) {
obj.opacity = this.originalNormalOpacities.get(key);
}
});
}
});
// 恢复活动图层
if (this.originalActiveLayerId) {
this.layerManager.setActiveLayer(this.originalActiveLayerId);
}
}
// 5. 重新启用渲染
this.canvas.renderOnAddRemove = true;
this.canvas.renderAll();
// 4. 恢复工具状态
if (this.toolManager && this.originalToolState) {
this.toolManager.isRedGreenMode =
this.originalToolState.isRedGreenMode;
if (this.originalToolState.currentTool) {
this.toolManager.setTool(this.originalToolState.currentTool);
}
}
});
return true;
} catch (error) {
@@ -226,35 +304,42 @@ export class BatchInitializeRedGreenModeCommand extends Command {
/**
* 设置背景图层
*/
async _setupBackgroundLayer(backgroundLayer) {
async _setupBackgroundLayer(backgroundLayer, clothingImage) {
let backgroundObject = backgroundLayer.fabricObject;
const { object } = findObjectById(this.canvas, backgroundObject.id);
if (!backgroundObject) {
if (!object) {
// 创建白色背景矩形
backgroundObject = new fabric.Rect({
left: 0,
top: 0,
width: this.canvas.width,
height: this.canvas.height,
fill: "#ffffff",
object = new fabric.Rect({
left: this.canvas.width / 2,
top: this.canvas.height / 2,
width: clothingImage.width,
height: clothingImage.height,
scaleX: clothingImage.scaleX,
scaleY: clothingImage.scaleY,
fill: "transparent", // 确保背景是透明的
selectable: false,
evented: false,
isBackground: true,
layerId: backgroundLayer.id,
layerName: backgroundLayer.name,
originX: "center",
originY: "center",
});
this.canvas.add(backgroundObject);
this.canvas.sendToBack(backgroundObject);
backgroundLayer.fabricObject = backgroundObject;
this.canvas.add(object);
this.canvas.sendToBack(object);
backgroundLayer.fabricObject = object;
} else {
// 更新现有背景对象大小
backgroundObject.set({
width: this.canvas.width,
height: this.canvas.height,
left: 0,
top: 0,
fill: "#ffffff", // 确保背景是白色
object.set({
width: clothingImage.width,
height: clothingImage.height,
scaleX: clothingImage.scaleX,
scaleY: clothingImage.scaleY,
left: this.canvas.width / 2,
top: this.canvas.height / 2,
fill: "transparent", // 确保背景是透明的
});
}
}
@@ -299,6 +384,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
evented: false,
layerId: fixedLayer.id,
layerName: fixedLayer.name,
id: generateId("clothingImage"),
});
// 清除固定图层原有内容
@@ -332,6 +418,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
evented: false,
layerId: normalLayer.id,
layerName: normalLayer.name,
id: generateId("redGreenImage"),
});
// 清除普通图层原有内容
@@ -341,6 +428,8 @@ export class BatchInitializeRedGreenModeCommand extends Command {
});
}
// 给img设置裁剪裁剪图为衣服底图
// 添加到画布和普通图层
this.canvas.add(img);
normalLayer.fabricObjects = [img];
@@ -354,13 +443,6 @@ export class BatchInitializeRedGreenModeCommand extends Command {
normalLayers.forEach((layer) => {
// 设置图层透明度
layer.opacity = this.normalLayerOpacity;
// 更新图层中所有对象的透明度
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
obj.opacity = this.normalLayerOpacity;
});
}
});
}

View File

@@ -1,5 +1,5 @@
import { Command, CompositeCommand } from "./Command.js";
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import { createLayer, LayerType } from "../utils/layerHelper.js";
/**

View File

@@ -1,5 +1,5 @@
<template>
<transition name="fade">
<transition name="brush-control-canel-fade">
<div v-if="isVisible" class="brush-control-panel">
<!-- 笔刷大小控制 -->
<VerticalSlider
@@ -55,80 +55,89 @@
</template>
</VerticalSlider>
<!-- 颜色选择器 - 仅在特定工具下显示 -->
<div v-if="showColorPicker" class="color-picker-container">
<label for="color-picker" class="current-color-label">
<div
class="current-color"
:style="{ backgroundColor: brushColor }"
></div>
</label>
<input
type="color"
id="color-picker"
class="system-color-picker"
v-model="customColor"
@input="setBrushColor(customColor)"
/>
</div>
<!-- 透明度控制 - 仅在特定工具下显示 -->
<VerticalSlider
v-if="showOpacitySlider"
v-model="brushOpacity"
:min="0"
:max="1"
:presets="opacityPresets"
:memorized-values="memorizedOpacities"
:is-percentage="true"
custom-class="opacity-slider"
:active-threshold="0.01"
:step="0.01"
v-model:showTooltip="showOpacityTooltip"
@slide-start="handleOpacitySlideStart"
@slide-end="handleOpacitySlideEnd"
@click="showOpacityTooltip = true"
>
<template #tooltip-content>
<div class="tooltip-header">
<div class="tooltip-title">Opacity</div>
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
<SvgIcon name="CClose" size="20" />
</div>
</div>
<div class="opacity-preview">
<div class="opacity-checker"></div>
<!-- 1.这里加上过渡动画 颜色选择器 - 仅在特定工具下显示 -->
<transition name="color-picker-fade" mode="out-in">
<div
v-if="showColorPicker"
class="color-picker-container"
key="color-picker"
>
<label for="color-picker" class="current-color-label">
<div
class="opacity-color"
:style="{
backgroundColor: brushColor,
opacity: brushOpacity,
}"
class="current-color"
:style="{ backgroundColor: brushColor }"
></div>
</div>
<div class="tooltip-content">
<div class="tooltip-text">
{{ Math.round(brushOpacity * 100) }}%
</label>
<input
type="color"
id="color-picker"
class="system-color-picker"
v-model="customColor"
@input="setBrushColor(customColor)"
/>
</div>
</transition>
<!-- 2.这里加上过渡动画 透明度控制 - 仅在特定工具下显示 -->
<transition name="opacity-slider-fade" mode="out-in">
<VerticalSlider
v-if="showOpacitySlider"
key="opacity-slider"
v-model="brushOpacity"
:min="0"
:max="1"
:presets="opacityPresets"
:memorized-values="memorizedOpacities"
:is-percentage="true"
custom-class="opacity-slider"
:active-threshold="0.01"
:step="0.01"
v-model:showTooltip="showOpacityTooltip"
@slide-start="handleOpacitySlideStart"
@slide-end="handleOpacitySlideEnd"
@click="showOpacityTooltip = true"
>
<template #tooltip-content>
<div class="tooltip-header">
<div class="tooltip-title">Opacity</div>
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
<SvgIcon name="CClose" size="20" />
</div>
</div>
<div class="tooltip-controls">
<button
class="control-btn add"
v-if="!memorizedOpacities.includes(brushOpacity)"
@click="memorizeOpacity"
>
+
</button>
<button
class="control-btn remove"
@click="removeMemorizedOpacity"
v-if="canRemoveOpacity"
>
-
</button>
<div class="opacity-preview">
<div class="opacity-checker"></div>
<div
class="opacity-color"
:style="{
backgroundColor: brushColor,
opacity: brushOpacity,
}"
></div>
</div>
</div>
</template>
</VerticalSlider>
<div class="tooltip-content">
<div class="tooltip-text">
{{ Math.round(brushOpacity * 100) }}%
</div>
<div class="tooltip-controls">
<button
class="control-btn add"
v-if="!memorizedOpacities.includes(brushOpacity)"
@click="memorizeOpacity"
>
+
</button>
<button
class="control-btn remove"
@click="removeMemorizedOpacity"
v-if="canRemoveOpacity"
>
-
</button>
</div>
</div>
</template>
</VerticalSlider>
</transition>
</div>
</transition>
</template>
@@ -139,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: {
@@ -462,6 +470,13 @@ watch(
color: #333;
user-select: none;
-webkit-user-select: none;
// 添加高度过渡动画
transition: height 0.3s ease-out, min-height 0.3s ease-out;
// overflow: hidden;
transform: translate3d(0, -50%, 0); // 确保使用3D变换以提高性能
opacity: 1;
}
// 笔刷大小预览相关样式
@@ -660,27 +675,49 @@ watch(
}
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
.brush-control-canel-fade-enter-active,
.brush-control-canel-fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
.brush-control-canel-fade-enter-from,
.brush-control-canel-fade-leave-to {
opacity: 0;
transform: translateX(-10px) translateY(-50%);
}
// 颜色选择器过渡动画
.color-picker-fade-enter-active,
.color-picker-fade-leave-active {
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
}
.color-picker-fade-enter-from,
.color-picker-fade-leave-to {
opacity: 0;
transform: translateY(-8px) scale(0.95);
}
// 透明度滑块过渡动画
.opacity-slider-fade-enter-active,
.opacity-slider-fade-leave-active {
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
}
.opacity-slider-fade-enter-from,
.opacity-slider-fade-leave-to {
opacity: 0;
transform: translateY(8px) scale(0.95);
}
// 响应式调整
@media (max-height: 600px) {
.brush-control-panel {
transform: translateY(-50%);
gap: 10px;
}
}
@media (max-width: 768px) {
.brush-control-panel {
left: 10px;
// padding: 12px;
padding: 12px 3px;
}
}
</style>

View File

@@ -1,338 +1,350 @@
<template>
<div class="brush-panel">
<div class="brush-panel-content">
<!-- 笔刷类型选择 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷类型</span>
</div>
<div class="brush-type-grid">
<div
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<div class="brush-panel" @click.stop="">
<div class="brush-panel-wrapper">
<div class="brush-panel-content">
<!-- 笔刷类型选择 -->
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
<div class="section-actions" v-if="category === '材质'">
<button class="action-btn" @click="showLibrary = !showLibrary">
<i class="icon-library">📚</i> 材质库
</button>
<span>笔刷类型</span>
</div>
<div class="brush-type-grid">
<div
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
<div class="slider-header">
<span>{{ prop.name }}</span>
<span class="slider-value">
{{ formatPropertyValue(prop) }}
</span>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
<div class="section-actions" v-if="category === '材质'">
<button class="action-btn" @click="showLibrary = !showLibrary">
<i class="icon-library">📚</i> 材质库
</button>
</div>
</div>
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
<div class="slider-header">
<span>{{ prop.name }}</span>
<span class="slider-value">
{{ formatPropertyValue(prop) }}
</span>
</div>
<div class="slider-container">
<input
type="range"
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
class="property-slider"
/>
</div>
<!-- 预设值按钮如果定义了预设 -->
<div v-if="prop.presets" class="property-presets">
<button
v-for="preset in prop.presets"
:key="preset"
@click="handlePropertyChange(prop.id, preset)"
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
>
{{ preset }}
</button>
</div>
</div>
<div class="slider-container">
<input
type="range"
</template>
<!-- 颜色选择器 -->
<template v-else-if="prop.type === 'color'">
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
</div>
<div class="color-row">
<input
type="color"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state
.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@click="handlePropertyChange(prop.id, color)"
></div>
</div>
</div>
</div>
</template>
<!-- 复选框 -->
<template v-else-if="prop.type === 'checkbox'">
<div class="checkbox-property">
<span>{{ prop.name }}</span>
<div class="toggle-switch">
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
</div>
</div>
</template>
<!-- 选择器 -->
<template v-else-if="prop.type === 'select'">
<div class="select-property">
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
class="property-slider"
/>
</div>
<!-- 预设值按钮如果定义了预设 -->
<div v-if="prop.presets" class="property-presets">
<button
v-for="preset in prop.presets"
:key="preset"
@click="handlePropertyChange(prop.id, preset)"
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
class="property-select"
>
{{ preset }}
</button>
<option
v-for="option in prop.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</div>
</template>
</template>
<!-- 颜色选择器 -->
<template v-else-if="prop.type === 'color'">
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<!-- 文件选择器(用于材质) -->
<template v-else-if="prop.type === 'file'">
<div class="file-property">
<div class="file-header">
<span>{{ prop.name }}</span>
</div>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
class="file-preview"
@click="handleFileSelect(prop.id)"
>
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
<div v-else class="no-file">点击上传材质图片</div>
</div>
<div class="file-actions">
<button
class="select-file-btn"
@click="handleFileSelect(prop.id)"
>
上传图片
</button>
<button
class="clear-file-btn"
@click="handlePropertyChange(prop.id, '')"
v-if="prop.value"
>
清除
</button>
</div>
</div>
<div class="color-row">
</template>
<!-- 材质网格选择器 -->
<template v-else-if="prop.type === 'texture-grid'">
<div class="texture-grid-property">
<div class="texture-grid-header">
<span>{{ prop.name }}</span>
</div>
<div class="texture-grid">
<div
v-for="texture in prop.options"
:key="texture.value"
class="texture-item"
:class="{ active: prop.value === texture.value }"
@click="handleTextureSelect(texture.value)"
>
<img
:src="texture.preview || texture.value"
:alt="texture.label"
class="texture-thumbnail"
/>
<span class="texture-label">{{ texture.label }}</span>
</div>
<!-- 自定义纹理上传按钮 -->
<div
class="texture-item upload-item"
@click="triggerTextureUpload"
>
<div class="upload-icon">
<span>+</span>
</div>
<span class="texture-label">上传纹理</span>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="textureFileInput"
type="file"
accept="image/*"
@change="handleTextureUpload"
style="display: none"
/>
</div>
</div>
</template>
<!-- 其他类型的属性 -->
<template v-else>
<div class="generic-property">
<span>{{ prop.name }}</span>
<input
type="color"
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@click="handlePropertyChange(prop.id, color)"
></div>
</div>
</div>
</div>
</template>
<!-- 复选框 -->
<template v-else-if="prop.type === 'checkbox'">
<div class="checkbox-property">
<span>{{ prop.name }}</span>
<div class="toggle-switch">
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
</div>
</div>
</template>
<!-- 选择器 -->
<template v-else-if="prop.type === 'select'">
<div class="select-property">
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="property-select"
>
<option
v-for="option in prop.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</template>
<!-- 文件选择器用于材质 -->
<template v-else-if="prop.type === 'file'">
<div class="file-property">
<div class="file-header">
<span>{{ prop.name }}</span>
</div>
<div class="file-preview" @click="handleFileSelect(prop.id)">
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
<div v-else class="no-file">点击上传材质图片</div>
</div>
<div class="file-actions">
<button
class="select-file-btn"
@click="handleFileSelect(prop.id)"
>
上传图片
</button>
<button
class="clear-file-btn"
@click="handlePropertyChange(prop.id, '')"
v-if="prop.value"
>
清除
</button>
</div>
</div>
</template>
<!-- 材质网格选择器 -->
<template v-else-if="prop.type === 'texture-grid'">
<div class="texture-grid-property">
<div class="texture-grid-header">
<span>{{ prop.name }}</span>
</div>
<div class="texture-grid">
<div
v-for="texture in prop.options"
:key="texture.value"
class="texture-item"
:class="{ active: prop.value === texture.value }"
@click="handleTextureSelect(texture.value)"
>
<img
:src="texture.preview || texture.value"
:alt="texture.label"
class="texture-thumbnail"
/>
<span class="texture-label">{{ texture.label }}</span>
</div>
<!-- 自定义纹理上传按钮 -->
<div class="texture-item upload-item" @click="triggerTextureUpload">
<div class="upload-icon">
<span>+</span>
</div>
<span class="texture-label">上传纹理</span>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="textureFileInput"
type="file"
accept="image/*"
@change="handleTextureUpload"
style="display: none;"
class="property-input"
/>
</div>
</div>
</template>
</template>
<!-- 其他类型的属性 -->
<template v-else>
<div class="generic-property">
<span>{{ prop.name }}</span>
<input
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="property-input"
/>
<!-- 属性描述提示 -->
<div v-if="prop.description" class="property-description">
{{ prop.description }}
</div>
</template>
<!-- 属性描述提示 -->
<div v-if="prop.description" class="property-description">
{{ prop.description }}
</div>
</div>
</div>
</div>
</template>
</template>
<!-- 材质库弹窗 -->
<div
v-if="showLibrary"
class="texture-library-overlay"
@click.self="showLibrary = false"
>
<div class="texture-library-modal">
<div class="modal-header">
<h3>材质库</h3>
<button class="close-btn" @click="showLibrary = false">
&times;
</button>
</div>
<div class="modal-content">
<div class="texture-categories">
<button
v-for="(category, index) in textureCategories"
:key="index"
:class="[
'category-btn',
{ active: selectedCategory === category },
]"
@click="selectedCategory = category"
>
{{ category }}
<!-- 材质库弹窗 -->
<div
v-if="showLibrary"
class="texture-library-overlay"
@click.self="showLibrary = false"
>
<div class="texture-library-modal">
<div class="modal-header">
<h3>材质库</h3>
<button class="close-btn" @click="showLibrary = false">
&times;
</button>
</div>
<div class="texture-list">
<div
v-for="(texture, index) in filteredTextures"
:key="index"
class="library-texture-item"
@click="selectLibraryTexture(texture.path)"
>
<img
:src="texture.thumbnail"
:alt="texture.name"
class="texture-thumbnail"
/>
<span class="texture-name">{{ texture.name }}</span>
<div class="modal-content">
<div class="texture-categories">
<button
v-for="(category, index) in textureCategories"
:key="index"
:class="[
'category-btn',
{ active: selectedCategory === category },
]"
@click="selectedCategory = category"
>
{{ category }}
</button>
</div>
<div class="texture-list">
<div
v-for="(texture, index) in filteredTextures"
:key="index"
class="library-texture-item"
@click="selectLibraryTexture(texture.path)"
>
<img
:src="texture.thumbnail"
:alt="texture.name"
class="texture-thumbnail"
/>
<span class="texture-name">{{ texture.name }}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="upload-btn"
@click="handleFileSelect('texturePath')"
>
上传新材质
</button>
</div>
</div>
<div class="modal-footer">
<button class="upload-btn" @click="handleFileSelect('texturePath')">
上传新材质
</div>
<!-- 笔刷预设 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<i class="save-icon">+</i>
</button>
</div>
</div>
</div>
<!-- 笔刷预设 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<i class="save-icon">+</i>
</button>
</div>
<div class="presets-container">
<div
v-for="(preset, index) in brushStore.state.presets"
:key="index"
class="preset-item"
@click="applyPresetWithCommand(index)"
>
<div class="presets-container">
<div
class="preset-color"
:style="{
backgroundColor: preset.color,
width: preset.size + 'px',
height: preset.size + 'px',
opacity: preset.opacity,
}"
></div>
<span class="preset-name">{{ preset.name }}</span>
v-for="(preset, index) in brushStore.state.presets"
:key="index"
class="preset-item"
@click="applyPresetWithCommand(index)"
>
<div
class="preset-color"
:style="{
backgroundColor: preset.color,
width: preset.size + 'px',
height: preset.size + 'px',
opacity: preset.opacity,
}"
></div>
<span class="preset-name">{{ preset.name }}</span>
</div>
</div>
</div>
</div>
@@ -448,10 +460,7 @@ function handleTextureSelect(textureId) {
// 触发纹理上传文件选择
function triggerTextureUpload() {
const fileInput = textureFileInput.value;
if (fileInput) {
fileInput.click();
}
handleFileSelect("texturePath");
}
// 处理纹理文件上传
@@ -461,15 +470,15 @@ async function handleTextureUpload(event) {
try {
// 验证文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
if (!file.type.startsWith("image/")) {
alert("请选择图片文件");
return;
}
// 验证文件大小 (限制为 5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
alert('文件大小不能超过 5MB');
alert("文件大小不能超过 5MB");
return;
}
@@ -478,8 +487,8 @@ async function handleTextureUpload(event) {
const brushManager = toolManager?.brushManager;
if (!texturePresetManager || !brushManager) {
console.error('缺少必要的管理器实例');
alert('系统错误:无法上传纹理');
console.error("缺少必要的管理器实例");
alert("系统错误:无法上传纹理");
return;
}
@@ -493,12 +502,11 @@ async function handleTextureUpload(event) {
});
await commandManager.execute(command);
// 清空文件输入,允许重复上传同一文件
event.target.value = '';
event.target.value = "";
} catch (error) {
console.error('纹理上传失败:', error);
console.error("纹理上传失败:", error);
alert(`上传失败: ${error.message}`);
}
}
@@ -673,29 +681,44 @@ onMounted(() => {
const brushStore = BrushStore;
</script>
<style scoped>
<style scoped lang="less">
.brush-panel {
position: absolute;
right: 20px;
top: 62px;
padding: 0;
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
width: 100%;
overflow-y: auto;
max-height: 85vh;
width: 30%;
/* overflow-y: auto; */
min-width: 280px;
border-left: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
position: relative;
animation: panelFadeIn 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
backdrop-filter: blur(2px); /* 添加模糊效果 */
-webkit-backdrop-filter: blur(2px);
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
z-index: 1000; /* 确保面板在最上层 */
.brush-panel-wrapper {
overflow-y: auto;
height: 100%;
max-height: 85vh; /* 限制最大高度 */
/*优化ios上的滚动效果*/
-webkit-overflow-scrolling: touch;
.brush-panel-content {
display: flex;
flex-direction: column;
margin: 0;
}
}
}
/* 添加指向整个面板的倒三角 */
.brush-panel::before {
content: "";
position: absolute;
top: -10px;
left: 20px;
top: -9px;
right: 58px;
width: 0;
height: 0;
border-left: 10px solid transparent;
@@ -716,12 +739,6 @@ const brushStore = BrushStore;
}
}
.brush-panel-content {
display: flex;
flex-direction: column;
margin: 0;
}
.brush-section {
padding: 0;
background-color: transparent;
@@ -746,9 +763,10 @@ const brushStore = BrushStore;
user-select: none;
position: relative;
transition: background-color 0.2s ease;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
}
.section-header::after {
/* .section-header::after {
content: "";
position: absolute;
right: 16px;
@@ -757,18 +775,18 @@ const brushStore = BrushStore;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #999; /* 更柔和的颜色 */
border-top: 6px solid #999;
transform: translateY(-50%);
transition: transform 0.25s ease;
}
} */
.section-header:hover {
/* .section-header:hover {
background-color: rgba(248, 249, 250, 1);
}
.section-header:hover::after {
border-top-color: #666;
}
} */
.section-actions {
display: flex;
@@ -1114,6 +1132,12 @@ const brushStore = BrushStore;
height: 34px;
}
@media screen and (max-width: 1024px) {
.property-select {
width: 120px;
}
}
.property-select:focus {
border-color: #4285f4;
outline: none;

View File

@@ -17,6 +17,7 @@ provide("brushStore", BrushStore);
const toolManager = inject("toolManager");
const layerManager = inject("layerManager");
const isShowLayerPanel = inject("isShowLayerPanel", ref(false));
const props = defineProps({
activeTool: String,
@@ -24,6 +25,7 @@ const props = defineProps({
canvasHeight: Number,
canvasColor: String,
brushSize: Number,
enabledRedGreenMode: Boolean,
});
const emit = defineEmits([
@@ -40,9 +42,9 @@ const showBrushPanel = ref(false);
const brushPanelRef = ref(null);
// 计算属性
const shouldShowBrushSettings = computed(() => {
return props.activeTool === OperationType.DRAW;
});
// const shouldShowBrushSettings = computed(() => {
// return props.activeTool === OperationType.DRAW;
// });
function updateCanvasSize() {
if (!layerManager) {
@@ -86,6 +88,11 @@ function updateCanvasColor() {
// 切换笔刷面板显示状态
function toggleBrushPanel() {
// 如果笔刷没有激活 则激活笔刷工具
if (toolManager?.activeTool !== OperationType.DRAW) {
toolManager.setToolWithCommand(OperationType.DRAW);
}
showBrushPanel.value = !showBrushPanel.value;
}
@@ -176,16 +183,37 @@ function syncBrushStoreToManager() {
// 点击外部时关闭笔刷面板
function handleClickOutside(event) {
if (
showBrushPanel.value &&
brushPanelRef.value &&
!brushPanelRef.value.contains(event.target) &&
!event.target.closest(".brush-selector")
) {
showBrushPanel.value = false;
// if (isShowLayerPanel.value) {
// // 如果点击的是图层面板或其内部元素,则不关闭
// if (event.target.closest(".layers-panel")) {
// return;
// }
// // 关闭图层面板
// isShowLayerPanel.value = false;
// }
if (showBrushPanel.value) {
// 检查是否点击了笔刷选择器按钮
if (event.target.closest(".brush-selector")) {
return;
}
// 检查是否点击了笔刷面板或其内部元素
if (event.target.closest(".brush-panel")) {
return;
}
// 如果都不是,则关闭面板
if (showBrushPanel.value) {
showBrushPanel.value = false;
}
}
}
function showLayerPanel() {
isShowLayerPanel.value = !isShowLayerPanel.value;
}
onMounted(() => {
// 获取工具管理器和笔刷管理器
const brushManager = toolManager?.brushManager;
@@ -229,94 +257,51 @@ onMounted(() => {
<template>
<div class="canvas-header">
<span class="canvas-title">Canvas</span>
<!-- 默认设置 -->
<div
v-if="
<div class="canvas-header-wrapper">
<span class="canvas-title">Canvas</span>
<!-- 默认设置 -->
<!-- v-if="
!activeTool ||
activeTool === OperationType.SELECT ||
activeTool === OperationType.PAN
"
class="canvas-settings"
>
<div class="setting-group">
<span class="setting-label">Width</span>
<input
type="text"
:value="canvasWidth"
class="setting-input"
@input="$emit('update:canvasWidth', Number($event.target.value))"
@change="updateCanvasSize"
/>
</div>
<div class="setting-group">
<span class="setting-label">Height</span>
<input
type="text"
:value="canvasHeight"
class="setting-input"
@input="$emit('update:canvasHeight', Number($event.target.value))"
@change="updateCanvasSize"
/>
</div>
<div class="setting-group">
<span class="setting-label">Color</span>
<div class="color-picker-wrapper">
" -->
<div class="canvas-settings" v-if="!props.enabledRedGreenMode">
<div class="setting-group">
<span class="setting-label">Width</span>
<input
type="color"
:value="canvasColor"
class="color-picker"
@input="$emit('update:canvasColor', $event.target.value)"
@change="updateCanvasColor"
type="text"
:value="canvasWidth"
class="setting-input"
@input="$emit('update:canvasWidth', Number($event.target.value))"
@change="updateCanvasSize"
/>
<span class="color-dropdown">▼</span>
</div>
<div class="setting-group">
<span class="setting-label">Height</span>
<input
type="text"
:value="canvasHeight"
class="setting-input"
@input="$emit('update:canvasHeight', Number($event.target.value))"
@change="updateCanvasSize"
/>
</div>
<div class="setting-group">
<span class="setting-label">Color</span>
<div class="color-picker-wrapper">
<input
type="color"
:value="canvasColor"
class="color-picker"
@input="$emit('update:canvasColor', $event.target.value)"
@change="updateCanvasColor"
/>
<span class="color-dropdown"></span>
</div>
</div>
</div>
</div>
<!-- 绘图工具设置 -->
<div v-if="shouldShowBrushSettings" class="canvas-settings">
<!-- 简化的笔刷控制UI -->
<!-- <div class="setting-group">
<span class="setting-label">大小:</span>
<input
type="range"
:value="BrushStore.state.size"
min="0.5"
max="100"
step="0.5"
class="size-slider"
@input="handleBrushSizeChange"
/>
<span class="size-value">{{ BrushStore.state.size }}px</span>
</div> -->
<div class="setting-group">
<span class="setting-label">笔刷:</span>
<div class="brush-selector" @click="toggleBrushPanel">
<div
class="brush-preview"
:style="{
backgroundColor: BrushStore.state.color,
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
opacity: BrushStore.state.opacity,
}"
></div>
<span class="brush-dropdown">▼</span>
</div>
<!-- 笔刷面板 -->
<div
v-if="showBrushPanel"
class="brush-panel-container"
ref="brushPanelRef"
>
<BrushPanel />
</div>
</div>
<div class="setting-group">
<span class="setting-label">颜色:</span>
<div class="color-picker-wrapper">
<input
@@ -327,64 +312,71 @@ onMounted(() => {
/>
<span class="color-dropdown"></span>
</div>
</div>
</div>
<!-- 文本工具设置 -->
<div v-if="activeTool === OperationType.TEXT" class="canvas-settings">
<div class="setting-group">
<span class="setting-label">Font:</span>
<select class="font-select">
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div class="setting-group">
<span class="setting-label">Size:</span>
<input
type="number"
class="setting-input"
value="16"
min="8"
max="72"
/>
</div>
<div class="setting-group">
<span class="setting-label">Color:</span>
<div class="color-picker-wrapper">
<input type="color" class="color-picker" value="#000000" />
<span class="color-dropdown">▼</span>
</div>
</div>
</div>
<!-- 上传工具设置 -->
<div v-if="activeTool === OperationType.UPLOAD" class="canvas-settings">
<div class="setting-group">
<span class="setting-label">Upload Type:</span>
<select class="setting-select">
<option value="image">Image</option>
<option value="vector">Vector Graphics</option>
</select>
</div>
</div> -->
</div>
<!-- 导出设置 -->
<div class="setting-group export-group">
<!-- <div class="setting-group export-group">
<span class="export-model-select">exportModel.select:</span>
<span class="export-model-dropdown"></span>
</div> -->
<!-- 绘图工具设置 -->
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
<div
class="btn"
:class="{ active: showBrushPanel }"
@click="toggleBrushPanel"
>
<!-- <span class="setting-label">笔刷:</span>/ -->
<div class="brush-selector">
<SvgIcon name="CBrushTop" size="22"></SvgIcon>
<!-- <div
class="brush-preview"
:style="{
backgroundColor: BrushStore.state.color,
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
opacity: BrushStore.state.opacity,
}"
></div> -->
<!-- <span class="brush-dropdown"></span> -->
</div>
<!-- 笔刷面板 -->
<div
v-if="showBrushPanel"
class="brush-panel-container"
ref="brushPanelRef"
>
<Teleport to="body">
<BrushPanel />
</Teleport>
</div>
</div>
<div
class="btn"
:class="{ active: isShowLayerPanel }"
@click="showLayerPanel"
>
<SvgIcon name="CLayout" size="26"></SvgIcon>
</div>
</div>
</div>
</template>
<style scoped>
<style scoped lang="less">
.canvas-header {
display: flex;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #e0e0e0;
user-select: none;
height: 52px;
overflow: hidden;
width: 100%;
.canvas-header-wrapper {
display: flex;
align-items: center;
flex: auto;
}
}
.canvas-title {
@@ -393,19 +385,44 @@ onMounted(() => {
margin-right: 30px;
display: flex;
align-items: center;
}
.canvas-title::before {
content: "⟳";
margin-right: 5px;
font-size: 14px;
color: #333;
// &:before {
// // /* content: "⟳";
// // margin-right: 5px;
// // font-size: 14px; */
// }
}
.canvas-settings {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
gap: 8px;
color: #213547;
.btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
background-color: #f0f0f0;
&.active,
&:active {
background-color: #e6f7ff;
color: #1890ff;
}
&:hover {
background-color: #e6f7ff;
// color: #1890ff;
}
}
}
.gap-20 {
gap: 20px;
}
.setting-group {
@@ -476,15 +493,15 @@ 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: flex;
// align-items: center;
// border: 1px solid #ddd;
// border-radius: 4px;
// padding: 5px;
// cursor: pointer;
// background-color: white;
// width: 80px;
// justify-content: space-between;
}
.brush-preview {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
import SvgIcon from "../../../SvgIcon/index.vue";
const props = defineProps({
visible: Boolean,
position: {
type: Object,
default: () => ({ x: 0, y: 0 }),
},
items: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["close", "select"]);
const menuRef = ref(null);
const adjustedPosition = ref({ x: 0, y: 0 });
const hoveredItem = ref(null);
const submenuPositions = ref(new Map());
const hideTimer = ref(null); // 添加隐藏定时器
// 计算菜单位置,处理边界问题
const calculatePosition = () => {
if (!menuRef.value || !props.visible) return;
const menu = menuRef.value;
const menuRect = menu.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let x = props.position.x;
let y = props.position.y;
// 右边界检测
if (x + menuRect.width > windowWidth - 10) {
x = x - menuRect.width;
}
// 底边界检测
if (y + menuRect.height > windowHeight - 10) {
y = windowHeight - menuRect.height - 10;
}
// 左边界检测
if (x < 10) {
x = 10;
}
// 顶边界检测
if (y < 10) {
y = 10;
}
adjustedPosition.value = { x, y };
};
// 计算子菜单位置
const calculateSubmenuPosition = (itemElement, itemIndex) => {
if (!itemElement || !menuRef.value) return { x: 0, y: 0, direction: "right" };
const itemRect = itemElement.getBoundingClientRect();
const menuRect = menuRef.value.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 预估子菜单宽度(可以根据实际情况调整)
const submenuWidth = 200;
const submenuHeight = 300; // 预估高度
let x = itemRect.right + 4;
// 直接使用菜单项相对于主菜单容器的偏移量
let y = itemElement.offsetTop;
let direction = "right";
// 右边界检测,如果右侧空间不足,显示在左侧
if (x + submenuWidth > windowWidth - 10) {
x = itemRect.left - submenuWidth - 4;
direction = "left";
}
// 底边界检测 - 基于子菜单的绝对位置检查
const absoluteSubmenuBottom = itemRect.top + submenuHeight;
if (absoluteSubmenuBottom > windowHeight - 10) {
// 计算可用的最大Y位置相对于主菜单
const maxAbsoluteY = windowHeight - submenuHeight - 10;
const maxRelativeY = maxAbsoluteY - menuRect.top;
y = Math.max(0, maxRelativeY);
}
// 左边界检测
if (x < 10) {
x = 10;
direction = "right";
}
// 确保 y 不为负数
if (y < 0) {
y = 0;
}
y = 0;
const position = { x, y, direction };
submenuPositions.value.set(itemIndex, position);
return position;
};
// 清除隐藏定时器
const clearHideTimer = () => {
if (hideTimer.value) {
clearTimeout(hideTimer.value);
hideTimer.value = null;
}
};
// 显示子菜单
const showSubmenu = (item, index, element) => {
if (item.children && item.children.length > 0) {
clearHideTimer();
hoveredItem.value = index;
nextTick(() => {
calculateSubmenuPosition(element, index);
});
}
};
// 隐藏子菜单(延迟)
const hideSubmenu = (index) => {
clearHideTimer();
hideTimer.value = setTimeout(() => {
if (hoveredItem.value === index) {
hoveredItem.value = null;
}
}, 150);
};
// 处理鼠标进入菜单项
const handleItemMouseEnter = (item, index, event) => {
const element = event.target.closest(".context-menu-item");
showSubmenu(item, index, element);
};
// 处理鼠标在菜单项内移动
const handleItemMouseMove = (item, index, event) => {
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
if (
item.children &&
item.children.length > 0 &&
hoveredItem.value !== index
) {
const element = event.target.closest(".context-menu-item");
showSubmenu(item, index, element);
}
};
// 处理鼠标离开菜单项
const handleItemMouseLeave = (item, index) => {
// 只有当有子菜单时才延迟隐藏
if (item.children && item.children.length > 0) {
hideSubmenu(index);
} else {
hoveredItem.value = null;
}
};
// 处理鼠标进入子菜单
const handleSubmenuMouseEnter = (index) => {
clearHideTimer();
hoveredItem.value = index;
};
// 处理鼠标离开子菜单
const handleSubmenuMouseLeave = (index) => {
hideSubmenu(index);
};
// 监听可见性和位置变化
watch([() => props.visible, () => props.position], () => {
if (props.visible) {
nextTick(() => {
calculatePosition();
});
} else {
hoveredItem.value = null;
submenuPositions.value.clear();
}
});
// 处理菜单项点击
const handleItemClick = (item, index) => {
if (item.disabled || item.type === "divider") return;
// 如果有子菜单,不关闭菜单
if (item.children && item.children.length > 0) {
return;
}
emit("select", item, index);
if (item.action) {
item.action();
}
emit("close");
};
// 处理子菜单项点击
const handleSubItemClick = (subItem, parentIndex, subIndex) => {
if (subItem.disabled || subItem.type === "divider") return;
emit("select", subItem, `${parentIndex}-${subIndex}`);
if (subItem.action) {
subItem.action();
}
emit("close");
};
// 处理外部点击关闭
const handleOutsideClick = (event) => {
if (menuRef.value && !menuRef.value.contains(event.target)) {
emit("close");
}
};
// 处理 ESC 键关闭
const handleEscKey = (event) => {
if (event.key === "Escape") {
emit("close");
}
};
onMounted(() => {
document.addEventListener("click", handleOutsideClick, true);
document.addEventListener("contextmenu", handleOutsideClick, true);
document.addEventListener("keydown", handleEscKey);
});
onUnmounted(() => {
document.removeEventListener("click", handleOutsideClick, true);
document.removeEventListener("contextmenu", handleOutsideClick, true);
document.removeEventListener("keydown", handleEscKey);
});
</script>
<template>
<teleport to="body">
<transition name="context-menu">
<div
v-if="visible"
ref="menuRef"
class="context-menu"
:style="{
top: `${adjustedPosition.y}px`,
left: `${adjustedPosition.x}px`,
}"
@contextmenu.prevent
>
<template v-for="(item, index) in items" :key="index">
<!-- 分隔线 -->
<div
v-if="item.type === 'divider'"
class="context-menu-divider"
></div>
<!-- 菜单项 -->
<div
v-else
class="context-menu-item"
:class="{
disabled: item.disabled,
danger: item.danger,
'has-children': item.children && item.children.length > 0,
hovered: hoveredItem === index,
}"
@click="handleItemClick(item, index)"
@mouseenter="handleItemMouseEnter(item, index, $event)"
@mousemove="handleItemMouseMove(item, index, $event)"
@mouseleave="handleItemMouseLeave(item, index)"
>
<span class="context-menu-icon" v-if="item.icon">
<SvgIcon
:name="item.icon"
size="14"
:style="{
transform: item.inverIcon ? `rotate(90deg)` : 'none',
}"
/>
</span>
<span class="context-menu-label">{{ item.label }}</span>
<span class="context-menu-shortcut" v-if="item.shortcut">
{{ item.shortcut }}
</span>
<span
class="context-menu-arrow"
v-if="item.children && item.children.length > 0"
>
<SvgIcon name="CRight" size="12" />
</span>
<!-- 子菜单 -->
<transition name="context-submenu">
<div
v-if="
item.children &&
item.children.length > 0 &&
hoveredItem === index
"
class="context-submenu"
:class="{
'submenu-left':
submenuPositions.get(index)?.direction === 'left',
}"
@mouseenter="handleSubmenuMouseEnter(index)"
@mouseleave="handleSubmenuMouseLeave"
>
<template
v-for="(subItem, subIndex) in item.children"
:key="subIndex"
>
<!-- 子菜单分隔线 -->
<div
v-if="subItem.type === 'divider'"
class="context-menu-divider"
></div>
<!-- 子菜单项 -->
<div
v-else
class="context-menu-item"
:class="{
disabled: subItem.disabled,
danger: subItem.danger,
}"
@click="handleSubItemClick(subItem, index, subIndex)"
>
<span class="context-menu-icon" v-if="subItem.icon">
<SvgIcon
:name="subItem.icon"
size="14"
:style="{
transform: subItem.inverIcon
? `rotate(90deg)`
: 'none',
}"
/>
</span>
<span class="context-menu-label">{{ subItem.label }}</span>
<span class="context-menu-shortcut" v-if="subItem.shortcut">
{{ subItem.shortcut }}
</span>
</div>
</template>
</div>
</transition>
</div>
</template>
</div>
</transition>
</teleport>
</template>
<style scoped lang="less">
@import "./contextMenu.less";
</style>

View File

@@ -0,0 +1,514 @@
<script setup>
import { ref, nextTick, computed, inject } from "vue";
import { Checkbox } from "ant-design-vue";
import { VueDraggable } from "vue-draggable-plus";
import SvgIcon from "../../../SvgIcon/index.vue";
import { isGroupLayer } from "../../utils/layerHelper";
// 设置组件名称,用于递归渲染
defineOptions({
name: "LayerItem",
});
const props = defineProps({
layer: {
type: Object,
required: true,
},
isChild: {
type: Boolean,
default: false,
},
isActive: {
type: Boolean,
default: false,
},
isSelected: {
type: Boolean,
default: false,
},
isMultiSelectMode: {
type: Boolean,
default: false,
},
isEditing: {
type: Boolean,
default: false,
},
editingName: {
type: String,
default: "",
},
canDelete: {
type: Boolean,
default: true,
},
thumbnailUrl: {
type: String,
default: null,
},
isHidenDragHandle: {
type: Boolean,
default: false,
},
expandedGroupIds: {
type: Set,
default: () => new Set(),
},
});
const emit = defineEmits([
"click",
"double-click",
"context-menu",
"checkbox-change",
"toggle-visibility",
"toggle-lock",
"delete",
"edit-confirm",
"edit-cancel",
"edit-keydown",
"touch-start",
"touch-move",
"touch-end",
"child-layers-sort",
"update-child-layers",
"toggle-group-expanded",
// 新增子图层专用事件
"toggle-child-visibility",
"toggle-child-lock",
"delete-child",
"rename-child",
// v-model相关事件
"update:editingName",
]);
const layerManager = inject("layerManager", null);
// 计算属性
const isGroupLayerType = computed(() => {
return isGroupLayer(props.layer);
});
// 计算属性:检查组图层是否展开
const isGroupExpanded = computed(() => {
return props.expandedGroupIds.has(props.layer.id);
});
// 获取子图层
const childLayers = computed(() => {
if (!isGroupLayerType.value) return [];
// 优先使用 layer.children 属性
if (props.layer.children && Array.isArray(props.layer.children)) {
return props.layer.children;
}
return [];
});
// 切换组图层展开/收起状态
const toggleGroupExpanded = () => {
emit("toggle-group-expanded", props.layer.id);
};
// 获取图层类型图标
function getLayerTypeIcon(layer) {
if (!layer) return "🖼️";
if (isGroupLayer(layer)) {
return "📁";
}
if (layer.fabricObject) {
switch (layer.fabricObject.type) {
case "image":
return "🖼️";
case "text":
return "📝";
case "rect":
return "▢";
case "circle":
return "⬤";
case "path":
return "✎";
default:
return "⬤";
}
}
return "🖼️";
}
function getLayerTypeText(layerType) {
const typeMap = {
EMPTY: "空图层",
TEXT: "文本",
IMAGE: "图片",
SHAPE: "形状",
GROUP: "组合",
BACKGROUND: "背景",
FIXED: "固定",
};
return typeMap[layerType] || "未知";
}
// 事件处理
function handleClick(event) {
emit("click", props.layer, event);
}
function handleDoubleClick(event) {
emit("double-click", props.layer, event);
}
function handleContextMenu(event) {
emit("context-menu", event, props.layer);
}
function handleCheckboxChange(event) {
emit("checkbox-change", props.layer.id, event);
}
function handleToggleVisibility() {
if (props.isChild) {
// 子图层需要传递父图层ID - 从父级组件获取
const parentId = props.layer.parentId || findParentLayerId();
emit("toggle-child-visibility", props.layer.id, parentId);
} else {
// 一级图层
emit("toggle-visibility", props.layer.id);
}
}
function handleToggleLock() {
if (props.isChild) {
// 子图层需要传递父图层ID - 从父级组件获取
const parentId = props.layer.parentId || findParentLayerId();
emit("toggle-child-lock", props.layer.id, parentId);
} else {
// 一级图层
emit("toggle-lock", props.layer);
}
}
function handleDelete() {
if (!props.canDelete) {
console.warn("当前图层无法删除:", props.layer.id);
return;
}
if (props.isChild) {
// 子图层删除需要传递父图层ID
const parentId = props.layer.parentId || findParentLayerId();
if (parentId) {
emit("delete-child", props.layer.id, parentId);
} else {
console.warn("无法找到子图层的父图层ID:", props.layer.id);
}
} else {
// 一级图层删除
emit("delete", props.layer.id);
}
}
function handleEditConfirm() {
if (props.isChild) {
// 子图层重命名需要传递父图层ID
const parentId = props.layer.parentId || findParentLayerId();
if (props.editingName && props.editingName.trim() && parentId) {
emit("rename-child", props.layer.id, parentId, props.editingName.trim());
} else if (!parentId) {
console.warn("无法找到子图层的父图层ID:", props.layer.id);
}
// 发送编辑取消事件,清理编辑状态
emit("edit-cancel");
} else {
// 一级图层重命名
emit("edit-confirm");
}
}
function handleEditCancel() {
emit("edit-cancel");
}
function handleEditKeydown(event) {
emit("edit-keydown", event); // 修复事件名称,从 "edit-keyboard" 改为 "edit-keydown"
}
function handleTouchStart(event) {
emit("touch-start", event, props.layer);
}
function handleTouchMove(event) {
emit("touch-move", event);
}
function handleTouchEnd(event) {
emit("touch-end", event);
}
function handleUpdateChildLayers(newChildren) {
// 更新当前组图层的children数组
console.log(
"更新子图层顺序:",
"父图层ID:",
props.layer.id,
"新顺序:",
newChildren
);
emit("update-child-layers", props.layer.id, newChildren);
}
// 子图层递归事件处理
function handleChildClick(childLayer, event) {
emit("click", childLayer, event);
}
function handleChildDoubleClick(childLayer, event) {
emit("double-click", childLayer, event);
}
function handleChildContextMenu(event, childLayer) {
emit("context-menu", event, childLayer);
}
function handleChildToggleVisibility(childLayerId) {
emit("toggle-visibility", childLayerId);
}
function handleChildToggleLock(childLayer) {
emit("toggle-lock", childLayer);
}
// 动画钩子函数
function onEnter(el) {
// 设置初始状态
el.style.height = "0";
el.style.opacity = "0";
el.style.paddingTop = "0";
el.style.paddingBottom = "0";
el.style.marginTop = "0";
el.style.marginBottom = "0";
el.style.overflow = "hidden";
// 强制重排
el.offsetHeight;
// 获取最终高度
el.style.height = "auto";
const finalHeight = el.scrollHeight;
el.style.height = "0";
// 执行动画
requestAnimationFrame(() => {
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
el.style.height = finalHeight + "px";
el.style.opacity = "1";
el.style.paddingTop = "";
el.style.paddingBottom = "";
el.style.marginTop = "";
el.style.marginBottom = "";
});
}
function onLeave(el) {
// 设置当前高度
el.style.height = el.scrollHeight + "px";
el.style.overflow = "hidden";
// 强制重排
el.offsetHeight;
// 执行收起动画
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
el.style.height = "0";
el.style.opacity = "0";
el.style.paddingTop = "0";
el.style.paddingBottom = "0";
el.style.marginTop = "0";
el.style.marginBottom = "0";
}
// 查找父图层ID的辅助方法 - 增强版本
function findParentLayerId() {
// 首先检查 layer 对象是否已经有 parentId 属性
if (props.layer.parentId) {
return props.layer.parentId;
}
// 如果没有,尝试从 layerManager 中查找
if (layerManager && layerManager.layers) {
for (const layer of layerManager.layers.value) {
if (
layer.children &&
Array.isArray(layer.children) &&
layer.children.some((child) => child.id === props.layer.id)
) {
return layer.id;
}
}
}
console.warn("无法找到图层的父图层:", props.layer.id);
return null;
}
</script>
<template>
<div>
<!-- 主图层项 -->
<div
:class="[
'layer-item',
{
'child-layer': isChild,
active: isActive,
selected: isSelected,
'group-layer': isGroupLayerType,
editing: isEditing,
'multi-select-mode': isMultiSelectMode,
invisible: !layer.visible,
locked: layer.locked,
'fixed-layer': layer.isBackground || layer.isFixed,
},
]"
@click="handleClick"
@dblclick="handleDoubleClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@contextmenu.prevent="handleContextMenu"
>
<!-- 拖拽手柄 -->
<div class="layer-drag-handle" :title="$t('拖拽排序')">
<SvgIcon
v-if="!isHidenDragHandle"
:name="isChild ? 'CSort' : 'CSort'"
:size="16"
></SvgIcon>
</div>
<!-- 图层头部 -->
<div class="layer-header">
<!-- 多选复选框 -->
<div
v-if="isMultiSelectMode && !isChild"
class="layer-checkbox"
@click.stop
>
<Checkbox :checked="isSelected" @change="handleCheckboxChange" />
</div>
<!-- 图层预览图标 -->
<div class="layer-review">
<img
v-if="thumbnailUrl"
:src="thumbnailUrl"
class="layer-thumbnail"
:alt="$t('图层预览')"
/>
<span v-else class="layer-type-icon">{{
getLayerTypeIcon(layer)
}}</span>
</div>
<!-- 图层名称 -->
<div class="layer-name-container" :title="layer.name">
<div class="layer-name-wrapper">
<span v-if="!isEditing" class="layer-name text-ellipsis">
{{ layer.name }}
</span>
<input
v-else
:value="editingName"
:data-layer-id="layer.id"
:data-child-layer-id="isChild ? layer.id : undefined"
class="layer-name-input"
@blur="handleEditConfirm"
@keydown="handleEditKeydown"
@click.stop
@input="$emit('update:editingName', $event.target.value)"
/>
</div>
</div>
<!-- 图层操作按钮 -->
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
<!-- 可见性切换 -->
<div
class="visibility-btn"
@click.stop="handleToggleVisibility"
:title="$t('显示/隐藏图层')"
>
<SvgIcon v-if="layer.visible" name="CEye" :size="16"></SvgIcon>
<SvgIcon v-else name="CUnEye" :size="16"></SvgIcon>
</div>
<!-- 锁定状态 -->
<span
v-if="layer.locked"
class="status-icon locked"
:class="{ disabled: layer.isBackground || layer.isFixed }"
:title="$t('锁定')"
@click.stop="handleToggleLock"
>
<SvgIcon name="CLock" :size="18"></SvgIcon>
</span>
<span
v-else
class="status-icon"
:title="$t('未锁定')"
@click.stop="handleToggleLock"
>
<SvgIcon name="CUnLock" :size="18"></SvgIcon>
</span>
<!-- 删除按钮 -->
<div
class="delete-btn"
:class="{ disabled: !canDelete }"
:title="$t('删除图层')"
@click.stop="handleDelete"
>
<SvgIcon name="CDelete" size="14"></SvgIcon>
</div>
</div>
<!-- 组图层展开/收起图标 -->
<div
v-if="isGroupLayerType && !isChild"
class="group-expand-icon"
@click.stop="toggleGroupExpanded"
@dblclick.stop=""
:title="isGroupExpanded ? $t('收起组') : $t('展开组')"
>
<SvgIcon
name="CRight"
:size="12"
:style="{
transform: isGroupExpanded ? 'rotate(45deg)' : 'rotate(0deg)',
transition: 'transform 0.2s',
}"
/>
</div>
<!-- 图层状态指示器 -->
<!-- <div v-if="!isChild" class="layer-status">
<span
v-if="isGroupLayerType"
class="status-icon group"
:title="$t('组图层')"
>
📁
</span>
</div> -->
</div>
</div>
</div>
</template>
<style scoped lang="less">
@import "./layersPanel.less";
</style>

View File

@@ -0,0 +1,328 @@
<script setup>
import { computed, inject } from "vue";
import { VueDraggable } from "vue-draggable-plus";
import LayerItem from "./LayerItem.vue";
defineOptions({
name: "LayersList",
});
const props = defineProps({
layers: Array,
activeLayerId: String,
sortableRootLayers: Array,
selectedLayerIds: Array,
isMultiSelectMode: Boolean,
editingLayerId: String,
editingLayerName: String,
thumbnailManager: Object,
groupName: String,
expandedGroupIds: Set, // 新增:展开状态集合
isChild: Boolean,
parentLayerId: String, // 新增父图层ID
});
const emit = defineEmits([
"layer-click",
"layer-double-click",
"context-menu",
"checkbox-change",
"toggle-visibility",
"toggle-lock",
"delete",
"edit-confirm",
"edit-cancel",
"edit-keydown",
"touch-start",
"touch-move",
"touch-end",
"update:editing-name",
"root-layers-sort",
"child-layers-sort",
"select-child-layer",
"start-child-layer-edit",
"child-context-menu",
"finish-child-layer-edit",
"cancel-child-layer-edit",
"child-layer-edit-keydown",
"toggle-group-expanded",
// 新增子图层专用事件
"toggle-child-visibility",
"toggle-child-lock",
"delete-child",
"rename-child",
]);
// 检查图层是否被选中
const isLayerSelected = (layerId) => {
return props.selectedLayerIds.includes(layerId);
};
// 获取图层缩略图URL
function getLayerThumbnail(layerId) {
if (props.thumbnailManager) {
return props.thumbnailManager.getLayerThumbnail(layerId);
}
return null;
}
// 事件转发方法
const forwardEvent = (eventName, ...args) => {
emit(eventName, ...args);
};
// 处理根级图层拖拽排序
const handleRootLayersSort = (event) => {
if (props.isChild) {
// 子图层事件处理
// 确保排序只影响当前组图层的children而不是全局layers
emit(
"child-layers-sort",
event,
props.sortableRootLayers,
props.parentLayerId
);
} else {
emit("root-layers-sort", event);
}
};
const canDeleteComputed = computed(() => {
// 如果是子图层,检查父图层是否可以删除
if (props.isChild) {
const parentLayer = props.layers.find(
(layer) => layer.id === props.parentLayerId
);
return parentLayer?.children?.length > 1;
}
// 否则直接返回根图层的可删除状态
return props.layers.length > 3;
});
</script>
<template>
<div class="layers-list">
<!-- 可排序的根级图层 -->
<VueDraggable
:model-value="sortableRootLayers"
@end="handleRootLayersSort"
class="sortable-layers"
:animation="200"
:disabled="false"
handle=".layer-drag-handle"
ghost-class="ghost"
chosen-class="chosen"
drag-class="drag"
:group="groupName"
>
<!-- 遍历可排序的根级图层 -->
<template v-for="(layer, index) in sortableRootLayers" :key="layer.id">
<div class="layer-group">
<!-- 使用 LayerItem 子组件 -->
<LayerItem
:layer="layer"
:is-child="isChild"
:is-active="layer.id === activeLayerId"
:is-selected="isLayerSelected(layer.id)"
:is-multi-select-mode="isMultiSelectMode"
:is-editing="editingLayerId === layer.id"
:editing-name="editingLayerName"
:can-delete="
canDeleteComputed &&
!layer.isBackground &&
!layer.isFixed &&
!layer.locked
"
:thumbnail-url="getLayerThumbnail(layer.id)"
:expanded-group-ids="expandedGroupIds"
@click="(...args) => forwardEvent('layer-click', ...args)"
@double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
<!-- 子图层列表 (递归渲染) -->
<div
v-if="
layer?.children?.length > 0 &&
!layer.isBackground &&
!layer.isFixed &&
expandedGroupIds?.has(layer.id)
"
class="child-layers"
>
<LayersList
:layers="layers"
:sortableRootLayers="layer.children"
:active-layer-id="activeLayerId"
:selected-layer-ids="selectedLayerIds"
:is-multi-select-mode="isMultiSelectMode"
:editing-layer-id="editingLayerId"
:editing-layer-name="editingLayerName"
:thumbnail-manager="thumbnailManager"
:expanded-group-ids="expandedGroupIds"
:isChild="true"
:parentLayerId="layer.id"
group-name="layers-child"
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
@layer-double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@root-layers-sort="
(...args) => forwardEvent('root-layers-sort', ...args)
"
@child-layers-sort="
(...args) => forwardEvent('child-layers-sort', ...args)
"
@select-child-layer="
(...args) => forwardEvent('select-child-layer', ...args)
"
@start-child-layer-edit="
(...args) => forwardEvent('start-child-layer-edit', ...args)
"
@child-context-menu="
(...args) => forwardEvent('child-context-menu', ...args)
"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@finish-child-layer-edit="
(...args) => forwardEvent('finish-child-layer-edit', ...args)
"
@cancel-child-layer-edit="
(...args) => forwardEvent('cancel-child-layer-edit', ...args)
"
@child-layer-edit-keydown="
(...args) => forwardEvent('child-layer-edit-keydown', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
</div>
</div>
</template>
</VueDraggable>
</div>
</template>
<style scoped lang="less">
// 从父组件的样式文件中继承相关样式
.layers-list {
flex: 1;
overflow-y: auto;
.sortable-layers {
min-height: 20px;
}
// .layer-group {
// // margin-bottom: 1px;
// }
.child-layers {
position: relative;
padding-left: 20px;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 20px;
background-color: #e0e0e0;
z-index: 9;
}
.layer-item {
margin-left: 8px;
}
}
.fixed-layers {
border-top: 1px solid #e0e0e0;
}
// 拖拽状态样式
.ghost {
opacity: 0.5;
background: #f0f0f0;
}
.chosen {
opacity: 0.8;
}
.drag {
opacity: 0.6;
}
}
// 响应式设计
@media (max-width: 768px) {
.layers-list {
.child-layers {
padding-left: 25px;
&::after {
width: 25px;
}
.layer-item {
margin-left: 8px;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,232 @@
// 右键菜单样式
.context-menu {
position: fixed;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
z-index: 1000;
max-width: 280px;
padding: 4px 0;
font-size: 14px;
// overflow: hidden;
top: 60px; // 默认位置,可根据实际需要调整
// 动画相关
&.context-menu-enter-active,
&.context-menu-leave-active {
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
&.context-menu-enter-from {
opacity: 0;
transform: scale(0.9);
}
&.context-menu-leave-to {
opacity: 0;
transform: scale(0.95);
}
}
.context-menu-item {
display: flex;
align-items: center;
padding: 5px 12px;
cursor: pointer;
transition: all 0.2s;
color: rgba(0, 0, 0, 0.85);
white-space: nowrap;
min-height: 32px;
position: relative;
&:hover:not(.disabled) {
background-color: #f5f5f5;
}
&:active:not(.disabled) {
background-color: #e6e6e6;
}
&.disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
.context-menu-icon {
color: rgba(0, 0, 0, 0.25);
}
}
&.danger {
color: #ff4d4f;
&:hover:not(.disabled) {
background-color: #fff2f0;
color: #ff7875;
}
.context-menu-icon {
color: #ff4d4f;
}
}
&.has-children {
position: relative;
}
&.hovered {
background-color: #f5f5f5;
}
}
.context-menu-icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
color: rgba(0, 0, 0, 0.65);
flex-shrink: 0;
}
.context-menu-label {
flex: 1;
line-height: 1.5;
}
.context-menu-shortcut {
margin-left: 16px;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.context-menu-arrow {
margin-left: 8px;
color: rgba(0, 0, 0, 0.45);
transition: transform 0.2s;
}
.context-menu-divider {
height: 1px;
margin: 4px 0;
background-color: rgba(0, 0, 0, 0.06);
}
// 子菜单样式
.context-submenu {
position: absolute;
left: 100%;
top: 0;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
z-index: 1001;
min-width: 160px;
max-width: 280px;
padding: 4px 0;
font-size: 14px;
// overflow: hidden;
&.submenu-left {
left: auto;
right: 100%;
margin-left: 0;
margin-right: 0;
}
}
// 子菜单动画
.context-submenu-enter-active,
.context-submenu-leave-active {
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.context-submenu-enter-from {
opacity: 0;
transform: scale(0.9) translateY(-8px);
}
.context-submenu-leave-to {
opacity: 0;
transform: scale(0.95) translateY(-4px);
}
// 响应式优化
@media (max-width: 768px) {
.context-menu {
min-width: 140px;
font-size: 13px;
}
.context-menu-item {
min-height: 36px;
padding: 6px 10px;
}
.context-menu-shortcut {
display: none;
}
.context-submenu {
min-width: 140px;
font-size: 13px;
}
}
// 暗色主题支持
@media (prefers-color-scheme: dark) {
.context-menu {
background-color: #1f1f1f;
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
0 3px 6px -4px rgba(0, 0, 0, 0.32),
0 9px 28px 8px rgba(0, 0, 0, 0.35);
}
.context-menu-item {
color: rgba(255, 255, 255, 0.85);
&:hover:not(.disabled) {
background-color: rgba(255, 255, 255, 0.08);
}
&:active:not(.disabled) {
background-color: rgba(255, 255, 255, 0.12);
}
&.disabled {
color: rgba(255, 255, 255, 0.3);
}
&.hovered {
background-color: rgba(255, 255, 255, 0.08);
}
}
.context-menu-icon {
color: rgba(255, 255, 255, 0.65);
}
.context-menu-shortcut {
color: rgba(255, 255, 255, 0.45);
}
.context-menu-arrow {
color: rgba(255, 255, 255, 0.45);
}
.context-menu-divider {
background-color: rgba(255, 255, 255, 0.12);
}
.context-submenu {
background-color: #1f1f1f;
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
0 3px 6px -4px rgba(0, 0, 0, 0.32),
0 9px 28px 8px rgba(0, 0, 0, 0.35);
}
}

View File

@@ -0,0 +1,844 @@
// 文本省略样式
.text-ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
// 主容器样式
.layers-panel-inner {
display: flex;
flex-direction: column;
user-select: none;
z-index: 6;
overflow-y: auto;
height: 100%;
max-height: 85vh;
-webkit-overflow-scrolling: touch;
}
// 头部样式
.layers-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #e0e0e0;
h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
}
.layer-actions-group {
display: flex;
gap: 8px;
.normal-actions,
.multi-select-actions {
display: flex;
gap: 8px;
}
}
// 操作按钮样式
.action-btn {
width: 32px;
height: 32px;
background: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: #f0f0f0;
border-color: #40a9ff;
color: #40a9ff;
}
&:active {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
background-color: #f5f5f5;
color: #bfbfbf;
border-color: #e6e6e6;
&:hover {
background-color: #f5f5f5;
color: #bfbfbf;
border-color: #e6e6e6;
}
}
}
// 特殊按钮样式
.group-btn {
background-color: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
&:hover {
background-color: #bae7ff;
border-color: #69c0ff;
}
&.disabled{
background-color: #f0f5ff;
border-color: #d9ecff;
color: #bfbfbf;
}
}
.ungroup-btn {
background-color: #fff2e8;
border-color: #ffbb96;
color: #fa8c16;
&:hover {
background-color: #ffd8bf;
border-color: #ff9c6e;
color: #fa8c16;
}
&.disabled{
background-color: #f0f5ff;
border-color: #d9ecff;
color: #bfbfbf;
}
}
.delete-selected-btn {
background-color: #fff2f0;
border-color: #ffccc7;
color: #ff4d4f;
&:hover {
background-color: #ffebe6;
border-color: #ff7875;
color: #ff4d4f;
}
}
.clear-selection-btn {
background-color: #f6f6f6;
border-color: #d9d9d9;
color: #595959;
&:hover {
background-color: #e6e6e6;
border-color: #bfbfbf;
color: #595959;
}
}
// 多选信息提示
.multi-select-info {
padding: 10px 6px;
// background-color: #e6f7ff;
background-color: rgba(238, 238, 238,0.4);
border-bottom: 1px solid #91d5ff;
color: #333;
font-size: 13px;
line-height: 1.4;
small {
display: block;
margin-top: 4px;
color: #666;
font-size: 11px;
}
}
// 图层列表
.layers-list {
position: relative;
flex: 1;
overflow-y: auto;
}
// 图层项样式
.layer-item {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
padding: 10px 0;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
border-left: 0;
border-right: 0;
border: none;
border-bottom: 1px solid #f5f2f2;
padding-left: 30px;
padding-right: 10px;
&.group-layer {
background-color: rgba(240, 248, 255, 0.3);
border-color: #e6f7ff;
}
&:hover {
background-color: #f0f0f0;
border-color: #e0e0e0;
}
&.active {
background-color: #e6f7ff;
border-color: #91d5ff;
}
&.selected {
background-color: #bae7ff;
border-color: #91d5ff;
// box-shadow: 0 0 0 1px #1890ff;
}
&.editing {
background-color: #fff7e6;
border-color: #ffd666;
}
// &.multi-select-mode {
// // padding-left: 30px;
// }
&.disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
// 图层头部
.layer-header {
display: flex;
align-items: center;
width: 100%;
gap: 6px;
}
// 图层预览
.layer-review {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex: none;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
border: 1px solid #e0e0e0;
}
.layer-thumbnail {
width: 100%;
height: 100%;
object-fit: contain;
}
.layer-type-icon {
font-size: 14px;
}
// 可见性按钮
.visibility-btn {
width: 22px;
height: 22px;
cursor: pointer;
flex: none;
color: #333;
display: flex;
align-items: center;
justify-content: center;
&.hidden {
color: #999;
}
}
// 图层名称
.layer-name-container {
flex: 1;
margin: 0 6px;
overflow: hidden;
// max-width: 204px;
.layer-name-wrapper{
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.layer-name {
color: #333;
font-size: 14px;
text-align: left;
display: block;
width: 100%;
}
.layer-name-input {
width: 100%;
padding: 2px 4px;
border: 1px solid #1890ff;
border-radius: 3px;
font-size: 14px;
color: #333;
background-color: #fff;
outline: none;
box-sizing: border-box;
&:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
// 图层状态
.layer-status {
display: flex;
align-items: center;
gap: 4px;
}
.status-icon {
font-size: 12px;
&.locked {
color: #1890ff;
}
&.group {
color: #fa8c16;
}
&.disabled {
color: #ccc;
cursor: not-allowed;
}
}
// 图层操作
.layer-actions {
display: flex;
gap: 6px;
}
.delete-btn {
font-size: 16px;
cursor: pointer;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ff4d4f;
}
&.disabled{
color: #ccc;
cursor: not-allowed;
// pointer-events: none;
}
}
// 拖拽手柄
.layer-drag-handle {
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 100%;
cursor: move;
flex: none;
display: flex;
align-items: center;
justify-content: center;
color: #999;
margin-right: 4px;
background: #eee;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
&:hover {
color: #333;
}
}
// 复选框
.layer-checkbox {
width: 18px;
height: 18px;
margin-right: 0;
cursor: pointer;
flex: none;
display: flex;
align-items: center;
// input[type="checkbox"] {
// width: 16px;
// height: 16px;
// cursor: pointer;
// accent-color: #1890ff;
// }
}
// 子图层样式
.child-layers {
}
.child-layer {
padding: 8px 20px 8px 32px;
background-color: rgba(240, 240, 240, 0.3);
border-left: 2px solid #e0e0e0;
&:hover {
background-color: rgba(224, 224, 224, 0.5);
}
&.active {
background-color: rgba(230, 247, 255, 0.5);
border-left: 2px solid #1890ff;
}
&.editing {
background-color: rgba(255, 247, 230, 0.5);
border-left: 2px solid #ffd666;
}
.layer-actions {
position: static;
display: flex;
gap: 4px;
button {
width: 20px;
height: 20px;
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #999;
border-radius: 2px;
transition: all 0.2s;
&:hover {
background-color: #f0f0f0;
color: #333;
}
}
}
}
.layer-indent {
width: 20px;
flex: none;
}
.layer-info {
flex: 1;
margin: 0 8px;
.layer-name {
font-size: 13px;
color: #333;
margin-bottom: 2px;
}
.layer-status {
display: flex;
align-items: center;
gap: 4px;
}
.layer-type {
font-size: 11px;
color: #999;
}
}
.child-drag-handle {
width: 16px;
height: 16px;
cursor: move;
flex: none;
display: flex;
align-items: center;
justify-content: center;
color: #999;
margin-right: 4px;
&:hover {
color: #333;
}
}
// 固定图层样式
.fixed-layers {
border-top: 1px solid #e0e0e0;
// background-color: #fafafa;
background-color: rgba(238, 238, 238,0.4);
.layer-drag-handle{
cursor: default;
}
}
.fixed-layer {
background-color: #fafafa;
// border-left: 3px solid #1890ff;
// &:hover {
// background-color: #e6f7ff;
// }
}
.fixed-layer-indicator {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #1890ff;
margin-right: 4px;
}
.background-indicator,
.fixed-indicator {
display: flex;
align-items: center;
justify-content: center;
margin-left: 4px;
}
.background-icon,
.fixed-icon {
font-size: 14px;
color: #999;
}
// 拖拽样式
.sortable-layers {
width: 100%;
}
.ghost {
opacity: 0.5;
background-color: #f0f0f0;
border: 2px dashed #1890ff;
}
.chosen {
background-color: #e6f7ff;
border: 1px solid #1890ff;
}
.drag {
opacity: 0.8;
transform: rotate(5deg);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
// 子图层拖拽样式
.child-layers {
.ghost {
opacity: 0.4;
background-color: #fff7e6;
border: 2px dashed #faad14;
}
.chosen {
background-color: #fff7e6;
border: 1px solid #faad14;
}
.drag {
opacity: 0.7;
transform: rotate(3deg);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
}
// 响应式设计
@media (max-width: 768px) {
.layers-panel-inner {
max-height: 70vh;
}
.layer-item {
padding: 12px;
padding-left: 35px;
}
.layer-header {
min-height: 40px;
}
.layer-drag-handle,
.visibility-btn {
width: 24px;
}
.layer-review {
width: 36px;
height: 36px;
}
.action-btn {
width: 36px;
height: 36px;
}
.multi-select-info {
// padding: 12px;
small {
font-size: 11px;
}
}
// .layer-name-container {
// // max-width: 182px;
// }
}
// iPad 优化
@media (min-width: 768px) and (max-width: 1024px) {
.layer-item {
padding: 10px;
padding-left: 30px;
}
.layer-drag-handle:hover,
.visibility-btn:hover {
background-color: rgba(0, 0, 0, 0.04);
border-radius: 4px;
}
}
// 触摸设备优化
@media (hover: none) and (pointer: coarse) {
.layer-item {
padding-left: 30px;
&:hover {
background-color: transparent;
}
&:active {
background-color: #f0f0f0;
}
}
.action-btn {
&:hover {
background-color: transparent;
border-color: #d9d9d9;
color: #666;
}
&:active {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
}
}
// 组图层展开/收起图标样式
.group-expand-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
border-radius: 2px;
transition: all 0.2s ease;
margin-right: 4px;
// &:hover {
// background-color: rgba(0, 0, 0, 0.1);
// }
// 展开/收起图标的过渡动画
.svg-icon {
transition: transform 0.2s ease;
}
}
// 组图层样式
.group-layer {
.layer-type-icon {
font-size: 14px;
}
}
// 子图层缩进和连接线
.child-layers {
position: relative;
&::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background-color: #e0e0e0;
}
}
.layer-indent {
width: 16px;
position: relative;
&::after {
content: '';
position: absolute;
left: 8px;
top: 50%;
width: 8px;
height: 1px;
background-color: #e0e0e0;
}
}
// 子图层展开/收起动画样式
.child-layers-expand-enter-active,
.child-layers-expand-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.child-layers-expand-enter-from {
height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
.child-layers-expand-leave-to {
height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
// 展开图标旋转动画优化
.group-expand-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
border-radius: 2px;
transition: all 0.2s ease;
margin-right: 4px;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
// 展开/收起图标的过渡动画
.svg-icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
// 子图层展开时的额外样式
.child-layers {
position: relative;
background: linear-gradient(135deg, rgba(240, 248, 255, 0.1) 0%, rgba(240, 248, 255, 0.05) 100%);
// border-radius: 4px;
// margin-top: 2px;
&::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background: linear-gradient(to bottom, #e0e0e0 0%, rgba(224, 224, 224, 0.3) 100%);
}
// 子图层项动画
.layer-item {
animation: slideInRight 0.2s ease-out;
animation-fill-mode: both;
&:nth-child(1) { animation-delay: 0.05s; }
&:nth-child(2) { animation-delay: 0.1s; }
&:nth-child(3) { animation-delay: 0.15s; }
&:nth-child(4) { animation-delay: 0.2s; }
&:nth-child(5) { animation-delay: 0.25s; }
}
}
// 子图层项进入动画
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 移动端动画优化
@media (hover: none) and (pointer: coarse) {
.child-layers-expand-enter-active,
.child-layers-expand-leave-active {
transition: all 0.25s ease;
}
.group-expand-icon {
&:hover {
transform: none;
background-color: transparent;
}
&:active {
transform: scale(0.9);
background-color: rgba(0, 0, 0, 0.1);
}
}
.child-layers .layer-item {
animation-duration: 0.15s;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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,
@@ -156,7 +155,7 @@ const redGreenToolsList = ref([
{
id: OperationType.RED_BRUSH,
title: "Red Brush (R)",
action: () => selectTool(OperationType.RED_BRUSH),
action: () => selectTool(OperationType.RED_BRUSH, true),
icon: { name: "CBrush", size: "24" },
class: "red-brush-btn",
style: { color: "#FF0000" },
@@ -164,7 +163,7 @@ const redGreenToolsList = ref([
{
id: OperationType.GREEN_BRUSH,
title: "Green Brush (G)",
action: () => selectTool(OperationType.GREEN_BRUSH),
action: () => selectTool(OperationType.GREEN_BRUSH, true),
icon: { name: "CBrush", size: "24" },
class: "green-brush-btn",
style: { color: "#00AA00" },
@@ -172,7 +171,7 @@ const redGreenToolsList = ref([
{
id: OperationType.ERASER,
title: "Eraser (E)",
action: () => selectTool(OperationType.ERASER),
action: () => selectTool(OperationType.ERASER, true),
icon: { name: "CEraser", size: "22" },
class: "eraser-btn",
},
@@ -197,8 +196,8 @@ const toolsList = computed(() => {
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
});
function selectTool(tool) {
emit("tool-selected", tool);
function selectTool(tool, isRedGreenMode = false) {
emit("tool-selected", tool, isRedGreenMode);
}
function triggerImageUpload() {
@@ -261,15 +260,15 @@ function handleKeyDown(event) {
switch (key) {
case "R":
selectTool(OperationType.RED_BRUSH);
selectTool(OperationType.RED_BRUSH, true);
event.preventDefault();
break;
case "G":
selectTool(OperationType.GREEN_BRUSH);
selectTool(OperationType.GREEN_BRUSH, true);
event.preventDefault();
break;
case "E":
selectTool(OperationType.ERASER);
selectTool(OperationType.ERASER, true);
event.preventDefault();
break;
}
@@ -323,6 +322,8 @@ onUnmounted(() => {
border-right: 1px solid #e0e0e0;
background-color: #ffffff;
user-select: none;
min-width: 58px;
height: 100%;
}
.tool-btn {

View File

@@ -6,6 +6,7 @@ import {
defineAsyncComponent,
shallowRef,
provide,
defineExpose,
} from "vue";
import { CanvasManager } from "./managers/CanvasManager";
import { LayerManager } from "./managers/LayerManager";
@@ -21,14 +22,14 @@ import { RedGreenModeManager } from "./managers/RedGreenModeManager";
// 导入封装的组件
import ToolsSidebar from "./components/ToolsSidebar.vue";
import HeaderMenu from "./components/HeaderMenu.vue";
import LayersPanel from "./components/LayersPanel.vue";
import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
import BrushControlPanel from "./components/BrushControlPanel.vue";
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 { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import { uploadImageAndCreateLayer } from "./utils/imageHelper.js";
// import MinimapPanel from "./components/MinimapPanel.vue";
const KeyboardShortcutHelp = defineAsyncComponent(() =>
@@ -70,13 +71,17 @@ const currentZoom = ref(100);
const canvasWidth = ref(CanvasConfig.width);
const canvasHeight = ref(CanvasConfig.height);
const canvasColor = ref(CanvasConfig.backgroundColor);
const layerWidth = ref(CanvasConfig.layerWidth); // 假设侧边栏宽度为 250px
const layerWidth = ref(CanvasConfig.layerWidth);
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
const canvasManagerLoaded = ref(false); // 画布是否加载完成
// 红绿图模式状态
const isRedGreenMode = ref(false);
const isShowLayerPanel = ref(true); // 是否显示图层面板
provide("isShowLayerPanel", isShowLayerPanel); // 提供红绿图模式状态给子组件
// 小地图设置
// const minimapEnabled = ref(true);
// const minimapManager = ref(null);
@@ -110,7 +115,9 @@ function toggleShortcutHelp() {
function handleToolSelect(tool) {
activeTool.value = tool;
// toolManager.setActiveTool(tool); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
toolManager.setToolWithCommand(tool); // 命令模式 可撤回操作
toolManager.setToolWithCommand(tool, {
undoable: props.enabledRedGreenMode ? false : true, // 普通模式下工具选择不可撤销
}); // 命令模式 可撤回操作
}
function toggleMinimap(enabled) {
@@ -127,9 +134,10 @@ onMounted(async () => {
canvasHeight.value = canvasContainerRef.value.clientWidth;
canvasWidth.value = canvasContainerRef.value.clientHeight;
}
// 创建管理器实例
canvasManager = new CanvasManager(canvasRef.value, {
width: canvasContainerRef.value.clientWidth - layerWidth.value, // 初始化的时候需要减去侧边栏宽度
width: canvasContainerRef.value.clientWidth,
height: canvasContainerRef.value.clientHeight,
// backgroundColor: canvasColor.value,
currentZoom,
@@ -139,7 +147,6 @@ onMounted(async () => {
canvasColor,
enabledRedGreenMode: props.enabledRedGreenMode,
});
console.log(canvasManager,canvasManager.thumbnailManager)
canvasManager.canvas.activeLayerId = activeLayerId;
canvasManager.canvas.activeElementId = activeElementId;
@@ -155,6 +162,7 @@ onMounted(async () => {
canvasWidth: canvasWidth.value,
canvasHeight: canvasHeight.value,
backgroundColor: canvasColor.value,
isRedGreenMode: props.enabledRedGreenMode,
layers,
activeLayerId,
canvasManager, // 添加对 canvasManager 的引用
@@ -202,6 +210,7 @@ onMounted(async () => {
layerManager.setToolManager(toolManager); // 将工具管理器传递给图层管理器
canvasManager.setToolManager(toolManager); // 将工具管理器传递给画布管理器
canvasManager.setLayerManager(layerManager);
canvasManager.setCommandManager(commandManager); // 将命令管理器传递给画布管理器
// 初始化快捷键管理器
keyboardManager = new KeyboardManager({
@@ -369,13 +378,18 @@ function updateCanvasSize() {
const containerWidth = canvasContainerRef.value.clientWidth;
const containerHeight = canvasContainerRef.value.clientHeight;
// 如果启用了红绿图模式,使用 layerManager 的缩放方法
if (props.enabledRedGreenMode && layerManager) {
layerManager.resizeCanvasWithScale(containerWidth, containerHeight);
} else {
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
canvasManager.setCanvasSize(containerWidth, containerHeight);
}
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
canvasManager.setCanvasSize(containerWidth, containerHeight);
// // 如果启用了红绿图模式,使用 layerManager 的缩放方法
// if (props.enabledRedGreenMode && layerManager) {
// layerManager.resizeCanvasWithScale(containerWidth, containerHeight, {
// undoable: false, // 可撤销操作
// });
// } else {
// // 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
// canvasManager.setCanvasSize(containerWidth, containerHeight);
// }
}
}
@@ -388,15 +402,17 @@ function addLayer() {
}
function setActiveLayer(layerId) {
if (activeElementId.value && canvasManager && canvasManager.canvas) {
if (layerId !== activeLayerId.value) {
layerManager.setActiveLayer(layerId, {
undoable: true, // 可撤销
});
const activeObject = canvasManager.canvas.getActiveObject();
if (activeObject) {
canvasManager.canvas.discardActiveObject();
canvasManager.canvas.renderAll();
}
activeElementId.value = null;
}
layerManager.setActiveLayer(layerId);
}
function toggleLayerVisibility(layerId) {
@@ -570,9 +586,9 @@ defineExpose({
getCanvasManager: () => canvasManager, // 获取画布管理器实例
canvasManagerLoaded,
// 加载新数据到画布
loadJSON: (json) => {
loadJSON: (json, calllBack) => {
try {
if (json) canvasManager?.loadJSON?.(json);
if (json) canvasManager?.loadJSON?.(json, calllBack);
return true;
} catch (error) {
console.error("加载画布JSON失败:", error);
@@ -611,42 +627,133 @@ defineExpose({
expPicType,
});
},
/**
* 移动图层位置
* @param {string} layerId 图层ID
* @param {string} direction 移动方向,'up'或'down'
* @returns {boolean} 是否移动成功
*/
moveLayer(layerId, direction) {
if (!layerManager) return false;
const result = layerManager.moveLayer(layerId, direction);
// 使用高级排序重建画布顺序
if (result) {
layerManager.forceRebuildCanvasOrder();
}
return result;
},
/**
* 拖拽排序图层
* @param {number} oldIndex 原索引
* @param {number} newIndex 新索引
* @param {string} layerId 图层ID
* @returns {boolean} 是否排序成功
*/
reorderLayers(oldIndex, newIndex, layerId) {
if (!layerManager) return false;
// 优先使用高级排序功能
if (layerManager.layerSort) {
return layerManager.advancedReorderLayers(oldIndex, newIndex, layerId);
} else {
// 降级到基础排序
return layerManager.reorderLayers(oldIndex, newIndex, layerId);
}
},
/**
* 智能排序图层
* 根据对象类型和位置自动调整图层顺序
* @param {Array<string>} targetLayerIds 要排序的图层ID数组null表示排序所有普通图层
* @returns {boolean} 是否排序成功
*/
smartSortLayers(targetLayerIds = null) {
if (!layerManager) return false;
return layerManager.smartSortLayers(targetLayerIds);
},
/**
* 优化图层结构
* 清理空图层、重新排序等
* @returns {Object} 优化结果统计
*/
optimizeLayerStructure() {
if (!layerManager)
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
return layerManager.optimizeLayerStructure();
},
/**
* 强制重建画布对象顺序
* 当图层顺序发生变化后调用此方法确保画布对象顺序正确
*/
forceRebuildCanvasOrder() {
if (!layerManager) return;
layerManager.forceRebuildCanvasOrder();
},
/**
* 验证画布对象顺序是否正确
* @returns {boolean} 顺序是否正确
*/
validateObjectOrder() {
if (!layerManager) return true;
return layerManager.validateObjectOrder();
},
/**
* 批量重新排序多个图层
* @param {Array} reorderOperations 排序操作数组 [{layerId, oldIndex, newIndex}]
* @returns {boolean} 是否全部操作成功
*/
batchReorderLayers(reorderOperations) {
if (!layerManager) return false;
return layerManager.batchReorderLayers(reorderOperations);
},
});
</script>
<template>
<div class="app-container">
<!-- 头部菜单组件 -->
<HeaderMenu
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:canvasWidth="canvasWidth"
:canvasHeight="canvasHeight"
:canvasColor="canvasColor"
:brushSize="brushSize"
@update:canvasWidth="canvasWidth = $event"
@update:canvasHeight="canvasHeight = $event"
@update:canvasColor="canvasColor = $event"
@update:brushSize="brushSize = $event"
@canvas-size-change="updateCanvasSize"
@canvas-color-change="updateCanvasColor"
/>
<div class="header-menu">
<HeaderMenu
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:canvasWidth="canvasWidth"
:canvasHeight="canvasHeight"
:canvasColor="canvasColor"
:brushSize="brushSize"
:enabledRedGreenMode="enabledRedGreenMode"
@update:canvasWidth="canvasWidth = $event"
@update:canvasHeight="canvasHeight = $event"
@update:canvasColor="canvasColor = $event"
@update:brushSize="brushSize = $event"
@canvas-size-change="updateCanvasSize"
@canvas-color-change="updateCanvasColor"
/>
</div>
<div class="main-content">
<!-- :minimapEnabled="minimapEnabled" -->
<!-- 工具栏组件 -->
<ToolsSidebar
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:isRedGreenMode="isRedGreenMode"
@tool-selected="handleToolSelect"
@red-green-tool-selected="handleRedGreenToolSelect"
@toggle-red-green-mode="toggleRedGreenMode"
@trigger-image-upload="triggerImageUpload"
@add-text="handleAddText"
@zoom-in="zoomIn"
@zoom-out="zoomOut"
/>
<div style="min-width: 58px">
<ToolsSidebar
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:isRedGreenMode="isRedGreenMode"
@tool-selected="handleToolSelect"
@red-green-tool-selected="handleRedGreenToolSelect"
@toggle-red-green-mode="toggleRedGreenMode"
@trigger-image-upload="triggerImageUpload"
@add-text="handleAddText"
@zoom-in="zoomIn"
@zoom-out="zoomOut"
/>
</div>
<div
class="canvas-container"
@@ -665,14 +772,14 @@ defineExpose({
<!-- 文本编辑面板 -->
<TextEditorPanel
v-if="canvasManagerLoaded"
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager?.canvas"
:commandManager="commandManager"
/>
<!-- 液化编辑面板 -->
<LiquifyPanel
v-if="canvasManagerLoaded"
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager?.canvas"
:commandManager="commandManager"
:liquifyManager="liquifyManager"
@@ -682,7 +789,7 @@ defineExpose({
<!-- 选区面板 -->
<SelectionPanel
v-if="canvasManagerLoaded"
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager?.canvas"
:commandManager="commandManager"
:selectionManager="selectionManager"
@@ -705,22 +812,29 @@ defineExpose({
</div>
<!-- 图层面板组件 -->
<LayersPanel
class="layers-panel"
:style="{ width: layerWidth + 'px' }"
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:activeLayerId="activeLayerId"
:activeElementId="activeElementId"
:thumbnailManager="canvasManager?.thumbnailManager"
@add-layer="addLayer"
@set-active-layer="setActiveLayer"
@toggle-layer-visibility="toggleLayerVisibility"
@move-layer-up="moveLayerUp"
@move-layer-down="moveLayerDown"
@remove-layer="removeLayer"
@layers-reorder="handleLayersReorder"
@child-layers-reorder="handleChildLayersReorder"
/>
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
<transition name="fade">
<div
class="layers-panel"
v-if="isShowLayerPanel && !enabledRedGreenMode"
>
<LayersPanel
v-if="canvasManagerLoaded"
:activeLayerId="activeLayerId"
:activeElementId="activeElementId"
:thumbnailManager="canvasManager.thumbnailManager"
@add-layer="addLayer"
@set-active-layer="setActiveLayer"
@toggle-layer-visibility="toggleLayerVisibility"
@move-layer-up="moveLayerUp"
@move-layer-down="moveLayerDown"
@remove-layer="removeLayer"
@layers-reorder="handleLayersReorder"
@child-layers-reorder="handleChildLayersReorder"
/>
</div>
</transition>
</div>
<div class="footer-actions">
@@ -769,6 +883,15 @@ defineExpose({
top: 0;
bottom: 0;
z-index: 77;
& > .header-menu {
height: 52px;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid #e0e0e0;
background-color: #ffffff;
}
}
.main-content {
@@ -972,10 +1095,33 @@ button:hover {
}
.layers-panel {
position: absolute;
right: 20px;
top: 10px;
transition: width 0.3s ease;
background: #fff;
width: 250px;
flex: none;
width: 350px;
max-height: 85vh;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
backdrop-filter: blur(2px); /* 添加模糊效果 */
-webkit-backdrop-filter: blur(2px);
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
z-index: 1000; /* 确保面板在最上层 */
border: 1px solid #e0e0e0;
/* 添加指向整个面板的倒三角 */
&::before {
content: "";
position: absolute;
top: -9px;
right: 6px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
z-index: 1;
}
}
/* 添加触控设备的样式调整 */
@media (pointer: coarse) {
@@ -1016,4 +1162,15 @@ button:hover {
font-size: 24px;
}
}
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>

View File

@@ -1,4 +1,4 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import initAligningGuidelines, {
initCenteringGuidelines,
} from "../utils/helperLine";
@@ -14,10 +14,15 @@ import { createCanvas } from "../utils/canvasFactory";
import { CanvasEventManager } from "./events/CanvasEventManager";
import CanvasConfig from "../config/canvasConfig";
import { RedGreenModeManager } from "./RedGreenModeManager";
import { EraserStateManager } from "./EraserStateManager";
import { deepClone, optimizeCanvasRendering } from "../utils/helper";
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
import { isFunction } from "lodash-es";
import {
ChangeFixedImageCommand,
AddImageToLayerCommand,
} from "../commands/ObjectLayerCommands";
restoreObjectLayerAssociations,
simplifyLayers,
validateLayerAssociations,
} from "../utils/layerUtils";
export class CanvasManager {
constructor(canvasElement, options) {
@@ -34,6 +39,7 @@ export class CanvasManager {
this.canvasHeight = options.canvasHeight || this.height; // 画布高度
this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色
this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式
this.eraserStateManager = null; // 橡皮擦状态管理器引用
// 初始化画布
this.initializeCanvas();
}
@@ -65,14 +71,6 @@ export class CanvasManager {
layers: this.layers,
});
// 初始化红绿图模式管理器
this.redGreenModeManager = new RedGreenModeManager({
canvas: this.canvas,
layerManager: null, // 稍后设置
toolManager: null, // 稍后设置
commandManager: null, // 稍后设置
});
// 设置画布辅助线
initAligningGuidelines(this.canvas);
@@ -90,7 +88,7 @@ export class CanvasManager {
*/
_initCanvasEvents() {
// 添加笔刷图像转换处理回调
this.canvas.onBrushImageConverted = (fabricImage) => {
this.canvas.onBrushImageConverted = async (fabricImage) => {
// 如果图层管理器存在,将图像合并到当前活动图层
if (this.layerManager) {
// 获取当前活动图层
@@ -107,21 +105,49 @@ export class CanvasManager {
});
// 执行高保真合并操作
this.eventManager?.mergeLayerObjectsForPerformance?.({
await this.eventManager?.mergeLayerObjectsForPerformance?.({
fabricImage,
activeLayer,
});
// 返回false表示不要自动添加到画布因为我们已经通过图层管理器处理了
return false;
// 返回true表示不要自动添加到画布因为我们已经通过图层管理器处理了
return true;
} else {
console.warn("没有活动图层,使用默认行为添加图像");
}
}
// 返回true表示使用默认行为直接添加到画布
return true;
// 返回false表示使用默认行为直接添加到画布
return false;
};
this.eraserStateManager = new EraserStateManager(
this.canvas,
this.layerManager
);
// 监听擦除开始事件
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?.(); // 如果没有命令管理器,直接执行命令
}
// 更新交互性
command &&
(await this.layerManager?.updateLayersObjectsInteractivity?.());
});
}
/**
@@ -168,6 +194,10 @@ export class CanvasManager {
if (this.redGreenModeManager) {
this.redGreenModeManager.layerManager = this.layerManager;
}
if (this.eraserStateManager) {
this.eraserStateManager.setLayerManager(this.layerManager);
}
}
/**
@@ -320,7 +350,8 @@ export class CanvasManager {
/**
* 居中所有画布元素
* 计算所有对象的边界框,然后将它们整体居中显示
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
* 这样可以保持对象间的相对位置关系不变
*/
centerAllObjects() {
if (!this.canvas) return;
@@ -333,45 +364,56 @@ export class CanvasManager {
(obj) => obj.visible !== false && !obj.excludeFromExport
);
// 如果只有背景层或没有可见对象,只居中背景层
if (
visibleObjects.length === 0 ||
(visibleObjects.length === 1 && visibleObjects[0].isBackground)
) {
// 尝试居中背景层
this.centerBackgroundLayer(this.width, this.height);
return;
}
// 如果没有可见对象,直接返回
if (visibleObjects.length === 0) return;
// 单独处理背景层
// 获取背景对象
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
const contentObjects = backgroundObject
? visibleObjects.filter((obj) => obj !== backgroundObject)
: visibleObjects;
// 如果只有背景层,居中背景层
if (contentObjects.length === 0 && backgroundObject) {
this.centerBackgroundLayer(this.width, this.height);
// 如果只有背景层或没有背景层,使用原有逻辑
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();
}
return;
}
// 计算内容对象的边界
const bounds = this._calculateObjectsBounds(contentObjects);
// 计算所有对象的中心点
const objectsCenterX = bounds.left + bounds.width / 2;
const objectsCenterY = bounds.top + bounds.height / 2;
// 记录背景层居中前的位置
const backgroundOldLeft = backgroundObject.left;
const backgroundOldTop = backgroundObject.top;
// 计算画布中心点
const canvasCenterX = this.width / 2;
const canvasCenterY = this.height / 2;
// 计算需要移动的距离
const deltaX = canvasCenterX - objectsCenterX;
const deltaY = canvasCenterY - objectsCenterY;
// 设置背景层居中
backgroundObject.set({
left: canvasCenterX,
top: canvasCenterY,
originX: "center",
originY: "center",
});
// 移动所有对象,包括背景层
visibleObjects.forEach((obj) => {
// 计算背景层的偏移量
const deltaX = backgroundObject.left - backgroundOldLeft;
const deltaY = backgroundObject.top - backgroundOldTop;
// 将相同的偏移量应用到所有其他对象上
const otherObjects = visibleObjects.filter(
(obj) => obj !== backgroundObject
);
otherObjects.forEach((obj) => {
obj.set({
left: obj.left + deltaX,
top: obj.top + deltaY,
@@ -456,6 +498,7 @@ export class CanvasManager {
// 如果需要裁剪背景层以外的内容,则更新蒙层位置
// 创建或更新蒙层
CanvasConfig.isCropBackground &&
!this.enabledRedGreenMode &&
this.createOrUpdateMask(backgroundLayerObject);
return true;
}
@@ -480,12 +523,13 @@ export class CanvasManager {
// 创建蒙层 - 使用透明矩形作为裁剪区域
this.maskLayer = new fabric.Rect({
id: "canvasMaskLayer",
width: bgWidth,
height: bgHeight,
left: left,
top: top,
fill: "transparent",
stroke: "#cccccc",
stroke: "transparent",
strokeWidth: 1,
strokeDashArray: [5, 5],
selectable: false,
@@ -504,20 +548,12 @@ export class CanvasManager {
this.canvas.clipPath = new fabric.Rect({
width: bgWidth,
height: bgHeight,
left: 0,
top: 0,
left: left,
top: top,
originX: backgroundLayerObject.originX || "left",
originY: backgroundLayerObject.originY || "top",
absolutePositioned: true,
});
// 设置蒙层位置
this.canvas.clipPath.set({
left: left,
top: top,
});
this.canvas.renderAll();
}
getBackgroundLayer() {
if (!this.canvas) return null;
@@ -655,6 +691,39 @@ export class CanvasManager {
});
}
/**
*
* 更改固定图层的图片
* @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;
}
const command = new ChangeFixedImageCommand({
canvas: this.canvas,
layerManager: this.layerManager,
imageUrl: imageUrl,
targetLayerType: options.targetLayerType || "fixed", // background/fixed
});
command.undoable =
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
return (
(await command?.execute?.()) || {
success: false,
layerId: null,
imageUrl: null,
}
);
}
/**
* 导出图片
* @param {Object} options 导出选项
@@ -663,6 +732,7 @@ export class CanvasManager {
* @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 导出的图片数据URL
*/
exportImage(options = {}) {
@@ -672,7 +742,39 @@ export class CanvasManager {
}
try {
return this.exportManager.exportImage(options);
// 自动设置红绿图模式相关参数
const enhancedOptions = {
...options,
// 如果没有明确指定,则根据当前模式自动设置
restoreOpacityInRedGreen:
options.restoreOpacityInRedGreen !== undefined
? options.restoreOpacityInRedGreen
: true, // 默认在红绿图模式下恢复透明度
};
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
if (
this.enabledRedGreenMode &&
!options.layerId &&
(!options.layerIdArray || options.layerIdArray.length === 0)
) {
console.log("检测到红绿图模式,自动包含所有普通图层进行导出");
// 获取所有非背景、非固定的普通图层ID
const normalLayerIds =
this.layers?.value
?.filter(
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
)
?.map((layer) => layer.id) || [];
if (normalLayerIds.length > 0) {
enhancedOptions.layerIdArray = normalLayerIds;
console.log("红绿图模式导出图层:", normalLayerIds);
}
}
return this.exportManager.exportImage(enhancedOptions);
} catch (error) {
console.error("CanvasManager导出图片失败:", error);
throw error;
@@ -709,129 +811,129 @@ export class CanvasManager {
}
getJSON() {
// 简化图层数据在loadJSON时要根据id恢复引用
let tempLayers = this.layers ? this.layers.value : [];
// // 简化图层数据在loadJSON时要根据id恢复引用
// let tempLayers = this.layers ? this.layers.value : [];
// // 创建对象ID映射表用于快速查找
// tempLayers = tempLayers.map((layer) => {
// const newLayer = { ...layer };
// 为所有fabric对象生成ID如果没有的话
const canvasObjects = this.canvas.getObjects();
canvasObjects.forEach((obj) => {
if (!obj.id) {
obj.id = `obj_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
}
});
// // 处理fabricObjects数组
// if (Array.isArray(layer.fabricObjects)) {
// newLayer.fabricObjects = layer.fabricObjects
// .map((item) => {
// if (!item) return null;
// 创建对象ID映射表,用于快速查找
const objectIdMap = new Map();
canvasObjects.forEach((obj) => {
if (obj.id) {
objectIdMap.set(obj, obj.id);
}
});
// // 确保对象ID
// if (!item.id) {
// item.id = `obj_${Date.now()}_${Math.floor(
// Math.random() * 10000
// )}`;
// }
tempLayers = tempLayers.map((layer) => {
const newLayer = { ...layer };
// return {
// id: item.id,
// type: item.type || "object", // 保存类型信息用于调试
// };
// })
// .filter((item) => item !== null);
// } else {
// newLayer.fabricObjects = [];
// }
// 处理fabricObjects数组
if (Array.isArray(layer.fabricObjects)) {
newLayer.fabricObjects = layer.fabricObjects
.map((item) => {
if (!item) return null;
// if (layer.clippingMask) {
// layer.clippingMask = {
// id: layer.clippingMask.id,
// };
// }
// 确保对象有ID
if (!item.id) {
item.id = `obj_${Date.now()}_${Math.floor(
Math.random() * 10000
)}`;
}
// // 处理单个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",
// };
// } else {
// newLayer.fabricObject = null;
// }
return {
id: item.id,
type: item.type || "object", // 保存类型信息用于调试
};
})
.filter((item) => item !== null);
} else {
newLayer.fabricObjects = [];
}
// // 处理子图层
// if (Array.isArray(layer.children)) {
// newLayer.children = layer.children.map((cItem) => {
// const newChild = { ...cItem };
// 处理单个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",
};
} else {
newLayer.fabricObject = null;
}
// // 处理子图层的fabricObjects
// if (Array.isArray(cItem.fabricObjects)) {
// newChild.fabricObjects = cItem.fabricObjects
// .map((item) => {
// if (!item) return null;
// 处理子图层
if (Array.isArray(layer.children)) {
newLayer.children = layer.children.map((cItem) => {
const newChild = { ...cItem };
// if (!item.id) {
// item.id = `obj_${Date.now()}_${Math.floor(
// Math.random() * 10000
// )}`;
// }
// 处理子图层的fabricObjects
if (Array.isArray(cItem.fabricObjects)) {
newChild.fabricObjects = cItem.fabricObjects
.map((item) => {
if (!item) return null;
// return {
// id: item.id,
// type: item.type || "object",
// };
// })
// .filter((item) => item !== null);
// } else {
// newChild.fabricObjects = [];
// }
if (!item.id) {
item.id = `obj_${Date.now()}_${Math.floor(
Math.random() * 10000
)}`;
}
// // 处理子图层的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 {
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;
});
// return newChild;
// });
// } else {
// newLayer.children = [];
// }
// return newLayer;
// });
try {
console.log(
"获取画布JSON数据...",
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
);
return JSON.stringify({
canvas: this.canvas.toJSON([
"id",
"type",
"layerId",
"layerName",
"isBackground",
"isLocked",
"isVisible",
"isFixed",
"parentId",
"excludeFromExport",
"eraser",
"eraserable",
"erasable",
]),
layers: tempLayers,
layers: JSON.stringify(
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
), // 简化图层数据
version: "1.0", // 添加版本信息
timestamp: new Date().toISOString(), // 添加时间戳
canvasWidth: this.canvasWidth.value,
@@ -845,7 +947,7 @@ export class CanvasManager {
}
}
loadJSON(json) {
loadJSON(json, calllBack) {
console.log("加载画布JSON数据:", json);
// 确保传入的json是字符串格式
@@ -859,7 +961,7 @@ export class CanvasManager {
const parsedJson = JSON.parse(json);
return new Promise((resolve, reject) => {
const tempLayers = parsedJson?.layers || [];
const tempLayers = JSON.parse(parsedJson?.layers) || [];
const canvasData = parsedJson?.canvas;
if (!tempLayers) {
@@ -872,9 +974,11 @@ export class CanvasManager {
return;
}
this.canvasWidth.value = parsedJson.canvasWidth || this.width;
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
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;
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
@@ -885,107 +989,57 @@ export class CanvasManager {
this.canvas.clear();
// 加载画布数据
this.canvas.loadFromJSON(canvasData, () => {
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
try {
// 重置画布数据
this.setCanvasSize(this.canvas.width, this.canvas.height);
this.canvas.loadFromJSON(canvasData, async () => {
await optimizeCanvasRendering(this.canvas, async () => {
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
try {
// 重置画布数据
this.setCanvasSize(this.canvas.width, this.canvas.height);
// 创建对象ID映射表用于快速查找
const objectIdMap = new Map();
const canvasObjects = this.canvas.getObjects();
// 重新构建对象关系
restoreObjectLayerAssociations(
this.layers.value,
this.canvas.getObjects()
);
canvasObjects.forEach((obj) => {
if (obj.id) {
objectIdMap.set(obj.id, obj);
}
});
// 验证图层关联关系 - 稳定后可以注释
const isValidate = validateLayerAssociations(
this.layers.value,
this.canvas.getObjects()
);
// 辅助函数根据ID查找对象
const findObjectById = (id) => {
if (!id) return null;
return objectIdMap.get(id) || null;
};
console.log("图层关联验证结果:", isValidate);
// 恢复图层数据
this.layers.value = tempLayers.map((layer) => {
const restoredLayer = { ...layer };
this.canvas.activeLayerId.value =
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
// 恢复fabricObjects数组
if (Array.isArray(layer.fabricObjects)) {
restoredLayer.fabricObjects = layer.fabricObjects
.map((item) => {
if (!item || !item.id) return null;
return findObjectById(item.id);
})
.filter((obj) => obj !== null);
} else {
restoredLayer.fabricObjects = [];
}
// // 如果检测到红绿图模式内容,进行缩放调整
// if (this.enabledRedGreenMode) {
// this._rescaleRedGreenModeContent();
// }
// 恢复单个fabricObject
if (layer.fabricObject && layer.fabricObject.id) {
restoredLayer.fabricObject = findObjectById(
layer.fabricObject.id
);
} else {
restoredLayer.fabricObject = null;
}
// 重载代码后支持回调中操作一些内容
await calllBack?.();
// 恢复子图层
if (Array.isArray(layer.children)) {
restoredLayer.children = layer.children.map((cItem) => {
const restoredChild = { ...cItem };
// 确保所有对象的交互性正确设置
await this.layerManager?.updateLayersObjectsInteractivity?.(
false
);
console.log(this.layerManager.layers.value);
debugger;
// 恢复子图层的fabricObjects
if (Array.isArray(cItem.fabricObjects)) {
restoredChild.fabricObjects = cItem.fabricObjects
.map((item) => {
if (!item || !item.id) return null;
return findObjectById(item.id);
})
.filter((obj) => obj !== null);
} else {
restoredChild.fabricObjects = [];
}
// 更新所有缩略图
setTimeout(() => {
this.updateAllThumbnails();
}, 100);
// 恢复子图层的fabricObject
if (cItem.fabricObject && cItem.fabricObject.id) {
restoredChild.fabricObject = findObjectById(
cItem.fabricObject.id
);
} else {
restoredChild.fabricObject = null;
}
return restoredChild;
});
} else {
restoredLayer.children = [];
}
return restoredLayer;
});
this.canvas.activeLayerId.value =
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
// 如果检测到红绿图模式内容,进行缩放调整
if (this.enabledRedGreenMode) {
this._rescaleRedGreenModeContent();
console.log("画布JSON数据加载完成");
resolve();
} catch (error) {
console.error("恢复图层数据失败:", error);
reject(new Error("恢复图层数据失败: " + error.message));
}
// 更新所有缩略图
setTimeout(() => {
this.updateAllThumbnails();
}, 100);
console.log("画布JSON数据加载完成");
resolve();
} catch (error) {
console.error("恢复图层数据失败:", error);
reject(new Error("恢复图层数据失败: " + error.message));
}
});
});
});
} catch (error) {

View File

@@ -0,0 +1,98 @@
import { EraserCommand } from "../commands/EraserCommand";
/**
* 橡皮擦状态管理器
* 用于管理橡皮擦操作的状态快照
*/
export class EraserStateManager {
constructor(canvas, layerManager) {
this.canvas = canvas;
this.layerManager = layerManager;
this.currentSnapshot = null;
this.pendingCommand = null;
}
setLayerManager(layerManager) {
this.layerManager = layerManager;
}
/**
* 开始橡皮擦操作 - 捕获初始状态
*/
startErasing() {
console.log("橡皮擦操作开始 - 捕获状态快照");
this.currentSnapshot = this._captureCanvasSnapshot();
}
/**
* 结束橡皮擦操作 - 创建命令
* @param {Array} affectedObjects 受影响的对象
* @returns {EraserCommand|null} 创建的橡皮擦命令
*/
endErasing(affectedObjects = []) {
if (!this.currentSnapshot) {
console.warn("没有初始状态快照,无法创建橡皮擦命令");
return null;
}
if (!affectedObjects || affectedObjects.length === 0) {
console.log("没有对象被擦除,不创建命令");
this.currentSnapshot = null;
return null;
}
console.log(`橡皮擦操作结束 - 影响了 ${affectedObjects.length} 个对象`);
// 捕获擦除后的状态
const afterSnapshot = this._captureCanvasSnapshot();
// 创建橡皮擦命令
const command = new EraserCommand({
canvas: this.canvas,
layerManager: this.layerManager,
affectedObjects: affectedObjects,
beforeSnapshot: this.currentSnapshot,
afterSnapshot: afterSnapshot,
});
// 重置状态
this.currentSnapshot = null;
return command;
}
/**
* 捕获画布状态快照
* @returns {Object} 画布状态快照
* @private
*/
_captureCanvasSnapshot() {
try {
return this.canvas.toJSON([
"id",
"type",
"layerId",
"layerName",
"isBackground",
"isLocked",
"isVisible",
"isFixed",
"parentId",
"eraser",
"eraserable",
"erasable",
]);
} catch (error) {
console.error("捕获画布状态快照失败:", error);
return null;
}
}
/**
* 取消当前擦除操作
*/
cancelErasing() {
this.currentSnapshot = null;
this.pendingCommand = null;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,441 @@
/**
* 液化对象引用管理器
* 专门处理液化操作中的对象引用管理,避免引用丢失问题
*/
export class LiquifyReferenceManager {
constructor() {
// 对象引用池
this.objectRefs = new Map();
// 状态快照池
this.stateSnapshots = new Map();
// ImageData缓存池
this.imageDataCache = new Map();
// 事件监听器备份
this.eventListeners = new Map();
}
/**
* 注册对象到引用管理器
* @param {Object} fabricObject Fabric对象
* @param {String} objectId 对象唯一ID
* @returns {String} 引用ID
*/
registerObject(fabricObject, objectId) {
const refId = objectId || this._generateRefId();
// 保存对象引用
this.objectRefs.set(refId, fabricObject);
// 备份事件监听器
this._backupEventListeners(refId, fabricObject);
// 创建初始状态快照
this._createStateSnapshot(refId, fabricObject);
console.log(`📝 对象已注册到引用管理器: ${refId}`);
return refId;
}
/**
* 获取对象引用
* @param {String} refId 引用ID
* @returns {Object|null} Fabric对象
*/
getObjectRef(refId) {
return this.objectRefs.get(refId) || null;
}
/**
* 更新对象的图像数据,保持引用不变
* @param {String} refId 引用ID
* @param {ImageData} newImageData 新的图像数据
* @returns {Promise<Boolean>} 更新结果
*/
async updateObjectImageData(refId, newImageData) {
const fabricObject = this.objectRefs.get(refId);
if (!fabricObject || !newImageData) {
throw new Error(`无法更新对象图像数据: 对象不存在或数据无效 (${refId})`);
}
return new Promise((resolve, reject) => {
try {
// 创建临时canvas
const tempCanvas = document.createElement("canvas");
tempCanvas.width = newImageData.width;
tempCanvas.height = newImageData.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.putImageData(newImageData, 0, 0);
// 创建新的图像元素
const img = new Image();
img.onload = () => {
try {
// 保存当前状态
const currentState = this._captureObjectState(fabricObject);
// 更新图像源
if (fabricObject.setElement) {
fabricObject.setElement(img);
} else if (fabricObject._element) {
fabricObject._element = img;
fabricObject._originalElement = img;
}
// 恢复非图像属性
this._restoreObjectState(fabricObject, currentState);
// 标记需要重新渲染
fabricObject.dirty = true;
fabricObject.setCoords();
// 更新缓存
this._updateImageDataCache(refId, newImageData);
console.log(`✅ 对象图像数据更新成功: ${refId}`);
resolve(true);
} catch (error) {
console.error("更新对象状态失败:", error);
reject(error);
}
};
img.onerror = () => {
reject(new Error("加载图像数据失败"));
};
img.src = tempCanvas.toDataURL();
} catch (error) {
console.error("创建临时canvas失败:", error);
reject(error);
}
});
}
/**
* 创建对象状态快照
* @param {String} refId 引用ID
* @param {String} snapshotId 快照ID
* @returns {String} 快照ID
*/
createSnapshot(refId, snapshotId = null) {
const fabricObject = this.objectRefs.get(refId);
if (!fabricObject) {
throw new Error(`无法创建快照: 对象不存在 (${refId})`);
}
const snapId = snapshotId || `${refId}_${Date.now()}`;
const snapshot = this._createStateSnapshot(snapId, fabricObject);
console.log(`📸 已创建对象快照: ${snapId}`);
return snapId;
}
/**
* 恢复对象到指定快照状态
* @param {String} refId 引用ID
* @param {String} snapshotId 快照ID
* @returns {Promise<Boolean>} 恢复结果
*/
async restoreFromSnapshot(refId, snapshotId) {
const fabricObject = this.objectRefs.get(refId);
const snapshot = this.stateSnapshots.get(snapshotId);
if (!fabricObject || !snapshot) {
throw new Error(
`无法恢复快照: 对象或快照不存在 (${refId}, ${snapshotId})`
);
}
// 恢复图像数据
if (snapshot.imageData) {
await this.updateObjectImageData(refId, snapshot.imageData);
}
// 恢复对象属性
if (snapshot.properties) {
this._restoreObjectState(fabricObject, snapshot.properties);
}
// 恢复事件监听器
if (snapshot.eventListeners) {
this._restoreEventListeners(refId, fabricObject, snapshot.eventListeners);
}
console.log(`🔄 对象快照恢复成功: ${refId} -> ${snapshotId}`);
return true;
}
/**
* 批量更新多个对象
* @param {Array} updates 更新列表 [{refId, imageData}, ...]
* @returns {Promise<Array>} 更新结果
*/
async batchUpdate(updates) {
const results = [];
for (const update of updates) {
try {
const result = await this.updateObjectImageData(
update.refId,
update.imageData
);
results.push({ refId: update.refId, success: true, result });
} catch (error) {
console.error(`批量更新失败 ${update.refId}:`, error);
results.push({
refId: update.refId,
success: false,
error: error.message,
});
}
}
return results;
}
/**
* 清理不再使用的引用
* @param {String} refId 引用ID
*/
cleanup(refId) {
this.objectRefs.delete(refId);
this.stateSnapshots.delete(refId);
this.imageDataCache.delete(refId);
this.eventListeners.delete(refId);
console.log(`🗑️ 已清理对象引用: ${refId}`);
}
/**
* 清理所有引用
*/
cleanupAll() {
this.objectRefs.clear();
this.stateSnapshots.clear();
this.imageDataCache.clear();
this.eventListeners.clear();
console.log("🗑️ 已清理所有对象引用");
}
/**
* 获取内存使用统计
* @returns {Object} 统计信息
*/
getMemoryStats() {
return {
objectRefs: this.objectRefs.size,
stateSnapshots: this.stateSnapshots.size,
imageDataCache: this.imageDataCache.size,
eventListeners: this.eventListeners.size,
totalMemoryUsage: this._calculateMemoryUsage(),
};
}
// 私有方法
/**
* 生成引用ID
* @returns {String} 引用ID
* @private
*/
_generateRefId() {
return `ref_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 备份事件监听器
* @param {String} refId 引用ID
* @param {Object} fabricObject Fabric对象
* @private
*/
_backupEventListeners(refId, fabricObject) {
const listeners = {};
// 备份常见的事件监听器
const eventTypes = [
"mousedown",
"mouseup",
"mousemove",
"mouseout",
"mouseover",
];
eventTypes.forEach((eventType) => {
if (
fabricObject.__eventListeners &&
fabricObject.__eventListeners[eventType]
) {
listeners[eventType] = [...fabricObject.__eventListeners[eventType]];
}
});
this.eventListeners.set(refId, listeners);
}
/**
* 恢复事件监听器
* @param {String} refId 引用ID
* @param {Object} fabricObject Fabric对象
* @param {Object} listeners 监听器备份
* @private
*/
_restoreEventListeners(refId, fabricObject, listeners) {
Object.keys(listeners).forEach((eventType) => {
if (listeners[eventType] && listeners[eventType].length > 0) {
listeners[eventType].forEach((listener) => {
fabricObject.on(eventType, listener);
});
}
});
}
/**
* 创建状态快照
* @param {String} snapId 快照ID
* @param {Object} fabricObject Fabric对象
* @returns {Object} 快照数据
* @private
*/
_createStateSnapshot(snapId, fabricObject) {
const snapshot = {
timestamp: Date.now(),
properties: this._captureObjectState(fabricObject),
imageData: this._captureImageData(fabricObject),
eventListeners: this.eventListeners.get(snapId.split("_")[0]) || {},
};
this.stateSnapshots.set(snapId, snapshot);
return snapshot;
}
/**
* 捕获对象状态
* @param {Object} fabricObject Fabric对象
* @returns {Object} 对象状态
* @private
*/
_captureObjectState(fabricObject) {
return {
left: fabricObject.left,
top: fabricObject.top,
scaleX: fabricObject.scaleX,
scaleY: fabricObject.scaleY,
angle: fabricObject.angle,
flipX: fabricObject.flipX,
flipY: fabricObject.flipY,
opacity: fabricObject.opacity,
visible: fabricObject.visible,
selectable: fabricObject.selectable,
evented: fabricObject.evented,
id: fabricObject.id,
layerId: fabricObject.layerId,
customData: fabricObject.customData ? { ...fabricObject.customData } : {},
filters: fabricObject.filters ? [...fabricObject.filters] : [],
};
}
/**
* 恢复对象状态
* @param {Object} fabricObject Fabric对象
* @param {Object} state 状态数据
* @private
*/
_restoreObjectState(fabricObject, state) {
if (!state) return;
fabricObject.set({
left: state.left,
top: state.top,
scaleX: state.scaleX,
scaleY: state.scaleY,
angle: state.angle,
flipX: state.flipX,
flipY: state.flipY,
opacity: state.opacity,
visible: state.visible,
selectable: state.selectable,
evented: state.evented,
});
// 恢复自定义属性
if (state.customData) {
fabricObject.customData = { ...state.customData };
}
// 恢复滤镜
if (state.filters && state.filters.length > 0) {
fabricObject.filters = [...state.filters];
fabricObject.applyFilters();
}
}
/**
* 捕获图像数据
* @param {Object} fabricObject Fabric对象
* @returns {ImageData|null} 图像数据
* @private
*/
_captureImageData(fabricObject) {
try {
if (
fabricObject._element &&
fabricObject._element.width &&
fabricObject._element.height
) {
const canvas = document.createElement("canvas");
canvas.width = fabricObject._element.width;
canvas.height = fabricObject._element.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(fabricObject._element, 0, 0);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}
} catch (error) {
console.warn("无法捕获图像数据:", error);
}
return null;
}
/**
* 更新图像数据缓存
* @param {String} refId 引用ID
* @param {ImageData} imageData 图像数据
* @private
*/
_updateImageDataCache(refId, imageData) {
this.imageDataCache.set(refId, {
data: imageData,
timestamp: Date.now(),
width: imageData.width,
height: imageData.height,
});
}
/**
* 计算内存使用量(近似值)
* @returns {Number} 内存使用量(字节)
* @private
*/
_calculateMemoryUsage() {
let totalBytes = 0;
// 计算ImageData缓存大小
this.imageDataCache.forEach((cache) => {
totalBytes += cache.width * cache.height * 4; // RGBA = 4 bytes per pixel
});
return totalBytes;
}
}
/**
* 创建单例引用管理器
*/
let liquifyReferenceManagerInstance = null;
export function getLiquifyReferenceManager() {
if (!liquifyReferenceManagerInstance) {
liquifyReferenceManagerInstance = new LiquifyReferenceManager();
}
return liquifyReferenceManagerInstance;
}

View File

@@ -1,4 +1,4 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
@@ -35,7 +35,6 @@ export class RedGreenModeManager {
* @param {String} options.redGreenImageUrl 红绿图URL
* @param {Number} options.normalLayerOpacity 普通图层透明度 (0-1)
* @param {Function} options.onImageGenerated 图片生成回调
* @param {Boolean} options.useBatchMode 是否使用批量模式 (默认true减少闪烁)
* @returns {Promise<boolean>} 是否初始化成功
*/
async initialize(options = {}) {
@@ -70,13 +69,8 @@ export class RedGreenModeManager {
throw new Error("缺少必需的图片URL参数");
}
// 使用批量模式或传统模式
const useBatchMode = options.useBatchMode !== false; // 默认为true
let initCommand;
// 使用新的批量初始化命令,减少页面闪烁
initCommand = new BatchInitializeRedGreenModeCommand({
const initCommand = new BatchInitializeRedGreenModeCommand({
canvas: this.canvas,
layerManager: this.layerManager,
toolManager: this.toolManager,
@@ -94,10 +88,11 @@ export class RedGreenModeManager {
await initCommand.execute();
}
this.registerRedGreenMouseUpEvent();
// 标记为已初始化
this.isInitialized = true;
this.registerRedGreenMouseUpEvent();
// 启用图层管理器的红绿图模式
if (
this.layerManager &&
@@ -106,6 +101,9 @@ export class RedGreenModeManager {
this.layerManager.enableRedGreenMode();
}
// 更新交互性
await this.layerManager?.updateLayersObjectsInteractivity?.();
// 重置工具管理器状态
// 默认红色笔刷
if (this.toolManager) {
@@ -116,7 +114,6 @@ export class RedGreenModeManager {
衣服底图: this.clothingImageUrl,
红绿图: this.redGreenImageUrl,
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
批量模式: useBatchMode ? "已启用" : "已禁用",
画布背景: "白色",
});
@@ -131,17 +128,17 @@ export class RedGreenModeManager {
// 注册鼠标抬起事件
registerRedGreenMouseUpEvent() {
this.canvas.on("mouse:up", (event) => {
if (!this.isInitialized) {
console.warn("红绿图模式未初始化,无法处理鼠标事件");
return;
}
// 可以在这里添加更多逻辑,比如生成图片或更新状态
if (this.onImageGenerated) {
const imageData = this.canvasManager.exportImage();
console.log("生成红绿图图片数据:", imageData);
this.onImageGenerated(imageData);
}
requestAnimationFrame(async () => {
if (!this.isInitialized) {
console.warn("红绿图模式未初始化,无法处理鼠标事件");
return;
}
if (this.onImageGenerated) {
const imageData = await this.canvasManager.exportImage();
this.onImageGenerated(imageData);
}
});
});
}

View File

@@ -3,11 +3,11 @@ import { BrushManager } from "./brushes/brushManager";
import { ToolCommand } from "../commands/ToolCommands";
import { OperationType } from "../utils/layerHelper";
import CanvasConfig from "../config/canvasConfig";
//import { fabric } from "fabric-with-all";
import {
InitLiquifyToolCommand,
RasterizeForLiquifyCommand,
} from "../commands/LiquifyCommands";
import { fabric } from "fabric-with-all";
import { InitLiquifyToolCommand } from "../commands/LiquifyCommands";
import { RasterizeLayerCommand } from "../commands/GroupCommands";
import { message, Modal } from "ant-design-vue";
import { h } from "vue";
/**
* 工具管理器
@@ -378,6 +378,8 @@ export class ToolManager {
previousTool: this.activeTool.value,
});
command.undoable = options.undoable !== undefined ? options.undoable : true;
// 执行命令
this.commandManager.execute(command, { ...options });
}
@@ -690,18 +692,9 @@ export class ToolManager {
}
} else if (checkResult.needsRasterization) {
// 需要栅格化 (多个对象或组)
// 询问用户是否要栅格化
if (
confirm(
checkResult.isGroup
? "组对象需要先栅格化才能进行液化操作,是否立即栅格化?"
: "当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
)
) {
// 用户确认栅格化,执行栅格化操作
this._rasterizeLayerForLiquify(activeLayerId);
return; // 栅格化后会重新调用液化功能,这里直接返回
}
// 使用Modal询问用户是否要栅格化
this._showRasterizeConfirmModal(checkResult.isGroup, activeLayerId);
return; // 等待用户确认,不继续执行
}
}
}
@@ -716,51 +709,42 @@ export class ToolManager {
}
/**
* 检查并准备液化操作
* 显示栅格化确认Modal对话框
* @param {Boolean} isGroup 是否为组对象
* @param {String} layerId 图层ID
* @private
*/
_checkAndPrepareForLiquify(layerId) {
// 确保存在液化管理器
const liquifyManager = this.canvasManager?.liquifyManager;
if (!liquifyManager) {
console.error("液化管理器未初始化");
return;
}
_showRasterizeConfirmModal(isGroup, layerId) {
const title = "栅格化图层";
const content = "需要先栅格化才能进行液化操作,是否立即栅格化?";
// 检查图层是否适合液化
const checkResult = liquifyManager.checkLayerForLiquify(layerId);
if (checkResult.isEmpty) {
// 空图层
alert("当前图层为空,无法进行液化操作");
return;
}
if (checkResult.isGroup) {
// 询问是否栅格化组
if (confirm("组对象需要栅格化才能进行液化操作,是否立即栅格化?")) {
Modal.confirm({
title,
content,
okText: "确定栅格化",
cancelText: "取消",
centered: true,
icon: h("span", { style: "color: #faad14;" }, "⚠️"),
onOk: () => {
// 用户确认栅格化,执行栅格化操作
this._rasterizeLayerForLiquify(layerId);
}
return;
}
if (checkResult.needsRasterization) {
// 询问是否栅格化图层
if (
confirm(
"当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
)
) {
this._rasterizeLayerForLiquify(layerId);
}
return;
}
// 如果图层可以直接液化(单个图像对象)
if (checkResult.valid) {
this._startLiquify(layerId);
}
},
onCancel: () => {
console.log("用户取消了栅格化操作");
// 用户取消,触发液化面板显示事件但不能液化
document.dispatchEvent(
new CustomEvent("showLiquifyPanel", {
detail: {
activeLayerId: layerId,
layerStatus: { needsRasterization: true, isGroup },
canLiquify: false,
targetObject: null,
originalImageData: null,
},
})
);
},
});
}
/**
@@ -772,25 +756,52 @@ export class ToolManager {
if (!this.commandManager || !this.layerManager) return;
try {
// 导入液化相关命令
// 显示加载Modal
const loadingModal = Modal.info({
title: "正在栅格化",
content: "正在栅格化图层,请稍候...",
okButtonProps: { style: { display: "none" } },
centered: true,
closable: false,
maskClosable: false,
});
// 创建栅格化命令
const rasterizeCommand = new RasterizeForLiquifyCommand({
const rasterizeCommand = new RasterizeLayerCommand({
canvas: this.canvas,
layerManager: this.layerManager,
layerId: layerId,
layers: this.layerManager.layers,
activeLayerId: this.layerManager.activeLayerId,
});
// 执行命令
const result = await this.commandManager.execute(rasterizeCommand);
// 关闭加载Modal
loadingModal.destroy();
if (result) {
// 栅格化成功,启动液化
message.success("图层已成功栅格化,可以进行液化操作");
this._startLiquify(layerId);
} else {
// 栅格化失败
Modal.error({
title: "栅格化失败",
content: "栅格化失败,无法进行液化操作",
okText: "确定",
centered: true,
});
}
} catch (error) {
console.error("栅格化图层失败:", error);
alert("栅格化失败,无法进行液化操作");
Modal.error({
title: "栅格化错误",
content: `栅格化失败:${error.message}`,
okText: "确定",
centered: true,
});
}
}
@@ -803,7 +814,12 @@ export class ToolManager {
// 获取图层信息
const layer = this.layerManager.getLayerById(layerId);
if (!layer) {
console.error("图层不存在");
Modal.error({
title: "图层错误",
content: "图层不存在",
okText: "确定",
centered: true,
});
return;
}
@@ -812,14 +828,24 @@ export class ToolManager {
if (layer.isBackground) {
// 背景图层使用 fabricObject (单数)
if (!layer.fabricObject) {
console.error("背景图层为空");
Modal.warning({
title: "背景图层为空",
content: "背景图层为空,无法进行液化操作",
okText: "确定",
centered: true,
});
return;
}
targetObject = layer.fabricObject;
} else {
// 普通图层使用 fabricObjects (复数)
if (!layer.fabricObjects || layer.fabricObjects.length === 0) {
console.error("图层为空");
Modal.warning({
title: "图层为空",
content: "图层为空,无法进行液化操作",
okText: "确定",
centered: true,
});
return;
}
targetObject = layer.fabricObjects[0];
@@ -828,11 +854,26 @@ export class ToolManager {
// 确保liquifyManager可用
const liquifyManager = this.canvasManager?.liquifyManager;
if (!liquifyManager) {
console.error("液化管理器未初始化");
Modal.error({
title: "液化管理器错误",
content: "液化管理器未初始化",
okText: "确定",
centered: true,
});
return;
}
try {
// 显示准备中的Modal
const preparingModal = Modal.info({
title: "准备液化环境",
content: "正在准备液化环境,请稍候...",
okButtonProps: { style: { display: "none" } },
centered: true,
closable: false,
maskClosable: false,
});
// 准备液化环境
liquifyManager.initialize({
canvas: this.canvas,
@@ -855,6 +896,9 @@ export class ToolManager {
// 执行初始化命令
await this.commandManager.execute(initCommand);
// 关闭准备Modal
preparingModal.destroy();
// 触发液化面板显示事件
document.dispatchEvent(
new CustomEvent("showLiquifyPanel", {
@@ -867,7 +911,12 @@ export class ToolManager {
);
} catch (error) {
console.error("启动液化工具失败:", error);
alert("启动液化工具失败:" + error.message);
Modal.error({
title: "液化工具启动失败",
content: `启动液化工具失败:${error.message}`,
okText: "确定",
centered: true,
});
}
}

View File

@@ -577,6 +577,49 @@ export class TexturePresetManager {
cacheSize: this.textureCache.size,
};
}
/**
* 验证纹理文件
* @param {File} file 要验证的文件
* @returns {Boolean} 是否为有效的纹理文件
*/
validateTextureFile(file) {
if (!file) {
console.warn("文件不存在");
return false;
}
// 检查文件类型
if (!file.type.startsWith("image/")) {
console.warn("文件类型无效,必须是图片文件");
return false;
}
// 检查文件大小(限制为 10MB
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
console.warn("文件大小超过限制10MB");
return false;
}
// 检查支持的图片格式
const supportedTypes = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
"image/svg+xml",
"image/bmp",
"image/gif",
];
if (!supportedTypes.includes(file.type)) {
console.warn("不支持的图片格式");
return false;
}
return true;
}
}
// 创建单例实例

View File

@@ -1,4 +1,5 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import "./fabric.brushes.js";
import { BrushStore } from "../../store/BrushStore";
import { brushRegistry } from "./BrushRegistry";
@@ -16,7 +17,7 @@ import { MarkerBrush } from "./types/MarkerBrush";
import { CustomPenBrush } from "./types/CustomPenBrush";
import { RibbonBrush } from "./types/RibbonBrush";
import { ShadedBrush } from "./types/ShadedBrush";
import { SketchyBrush } from "./types/SketchyBrush";
// import { SketchyBrush } from "./types/SketchyBrush";
import { SpraypaintBrush } from "./types/SpraypaintBrush";
/**
@@ -42,6 +43,11 @@ export class BrushManager {
// 初始化笔刷注册
this._registerDefaultBrushes();
// 初始化橡皮擦状态管理器
this.eraserStateManager = null;
this.isErasingActive = false;
this.currentErasedObjects = []; // 当前擦除会话中被影响的对象
}
/**
@@ -51,64 +57,112 @@ export class BrushManager {
_registerDefaultBrushes() {
// 注册铅笔笔刷
brushRegistry.register("pencil", PencilBrush, {
name: "铅笔",
name: "Pencil",
description: "基础铅笔工具,适合精细线条绘制",
category: "基础笔刷",
});
// 注册材质笔刷
brushRegistry.register("texture", TextureBrush);
brushRegistry.register("texture", TextureBrush, {
name: "Texture",
description: "使用纹理图片作为笔刷,支持缩放和透明度",
category: "基础笔刷",
});
// 注册集成的笔刷类型
brushRegistry.register("crayon", CrayonBrush);
brushRegistry.register("fur", FurBrush);
brushRegistry.register("ink", InkBrush);
brushRegistry.register("longfur", LongfurBrush);
brushRegistry.register("writing", WritingBrush);
brushRegistry.register("marker", MarkerBrush);
brushRegistry.register("pen", CustomPenBrush);
brushRegistry.register("ribbon", RibbonBrush);
brushRegistry.register("shaded", ShadedBrush);
brushRegistry.register("sketchy", SketchyBrush);
brushRegistry.register("spraypaint", SpraypaintBrush);
brushRegistry.register("crayon", CrayonBrush, {
name: "Crayon",
description: "使用纹理图片作为笔刷,支持缩放和透明度",
category: "基础笔刷",
});
brushRegistry.register("fur", FurBrush, {
name: "Texture",
description: "使用纹理图片作为笔刷,支持缩放和透明度",
category: "基础笔刷",
});
brushRegistry.register("ink", InkBrush, {
name: "Ink",
description: "墨水笔刷,适合书写和绘图",
category: "基础笔刷",
});
brushRegistry.register("", LongfurBrush, {
name: "Longfur",
description: "长毛发笔刷,适合绘制动物毛皮、草或头发",
category: "基础笔刷",
});
brushRegistry.register("writing", WritingBrush, {
name: "Writing",
description: "书法笔刷,模拟中国传统书法毛笔效果,具有笔锋和墨色变化",
category: "基础笔刷",
});
brushRegistry.register("marker", MarkerBrush, {
name: "Marker",
description: "马克笔笔刷,适合粗线条和填充",
category: "基础笔刷",
});
brushRegistry.register("pen", CustomPenBrush, {
name: "Pen",
description: "自定义钢笔笔刷,适合书写和绘图",
category: "基础笔刷",
});
brushRegistry.register("ribbon", RibbonBrush, {
name: "Ribbon",
description: "丝带笔刷,适合创建流动的丝带效果",
category: "基础笔刷",
});
brushRegistry.register("shaded", ShadedBrush, {
name: "Shaded",
description: "阴影笔刷,适合创建渐变和阴影效果",
category: "基础笔刷",
});
// brushRegistry.register("sketchy", SketchyBrush);
brushRegistry.register("spraypaint", SpraypaintBrush, {
name: "Spraypaint",
description: "喷漆笔刷,模拟喷漆效果",
category: "基础笔刷",
});
// 注册喷枪笔刷
brushRegistry.register(
"spray",
class SprayBrush extends PencilBrush {
constructor(canvas, options = {}) {
super(canvas, {
id: "spray",
name: "喷枪",
description: "模拟喷枪效果,创建散点效果",
category: "基础笔刷",
...options,
});
}
// // 注册喷枪笔刷
// brushRegistry.register(
// "spray",
// class SprayBrush extends PencilBrush {
// constructor(canvas, options = {}) {
// super(canvas, {
// id: "spray",
// name: "喷枪",
// description: "模拟喷枪效果,创建散点效果",
// category: "基础笔刷",
// ...options,
// });
// }
create() {
this.brush = new fabric.SprayBrush(this.canvas);
this.configure(this.brush, this.options);
return this.brush;
}
// create() {
// this.brush = new fabric.SprayBrush(this.canvas);
// this.configure(this.brush, this.options);
// return this.brush;
// }
configure(brush, options = {}) {
super.configure(brush, options);
// configure(brush, options = {}) {
// super.configure(brush, options);
if (options.density !== undefined) {
brush.density = options.density;
}
// if (options.density !== undefined) {
// brush.density = options.density;
// }
if (options.randomOpacity !== undefined) {
brush.randomOpacity = options.randomOpacity;
}
// if (options.randomOpacity !== undefined) {
// brush.randomOpacity = options.randomOpacity;
// }
if (options.dotWidth !== undefined) {
brush.dotWidth = options.dotWidth;
}
}
}
);
// if (options.dotWidth !== undefined) {
// brush.dotWidth = options.dotWidth;
// }
// }
// },
// {
// name: "喷枪",
// description: "模拟喷枪效果,创建散点效果",
// }
// );
// 注册橡皮擦笔刷
brushRegistry.register(
"eraser",
@@ -187,87 +241,87 @@ export class BrushManager {
}
);
// 注册水彩笔刷
brushRegistry.register(
"watercolor",
class WatercolorBrush extends PencilBrush {
constructor(canvas, options = {}) {
super(canvas, {
id: "watercolor",
name: "水彩",
description: "模拟水彩效果,带有流动感和透明感",
category: "特效笔刷",
...options,
});
}
// // 注册水彩笔刷
// brushRegistry.register(
// "watercolor",
// class WatercolorBrush extends PencilBrush {
// constructor(canvas, options = {}) {
// super(canvas, {
// id: "watercolor",
// name: "水彩",
// description: "模拟水彩效果,带有流动感和透明感",
// category: "特效笔刷",
// ...options,
// });
// }
create() {
// 创建一个自定义的PencilBrush来模拟水彩效果
this.brush = new fabric.PencilBrush(this.canvas);
this.configure(this.brush, this.options);
// create() {
// // 创建一个自定义的PencilBrush来模拟水彩效果
// this.brush = new fabric.PencilBrush(this.canvas);
// this.configure(this.brush, this.options);
// 水彩效果特有的属性
this.brush.globalCompositeOperation = "multiply";
this.brush.shadow = new fabric.Shadow({
color: this.options.color || "#000",
blur: 5,
offsetX: 0,
offsetY: 0,
});
// // 水彩效果特有的属性
// this.brush.globalCompositeOperation = "multiply";
// this.brush.shadow = new fabric.Shadow({
// color: this.options.color || "#000",
// blur: 5,
// offsetX: 0,
// offsetY: 0,
// });
return this.brush;
}
// return this.brush;
// }
configure(brush, options = {}) {
super.configure(brush, options);
// configure(brush, options = {}) {
// super.configure(brush, options);
// 水彩笔刷特有的配置
brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
}
}
);
// // 水彩笔刷特有的配置
// brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
// }
// }
// );
// 注册粉笔笔刷
brushRegistry.register(
"chalk",
class ChalkBrush extends PencilBrush {
constructor(canvas, options = {}) {
super(canvas, {
id: "chalk",
name: "粉笔",
description: "模拟粉笔效果,有颗粒感和不连续性",
category: "特效笔刷",
...options,
});
}
// // 注册粉笔笔刷
// brushRegistry.register(
// "chalk",
// class ChalkBrush extends PencilBrush {
// constructor(canvas, options = {}) {
// super(canvas, {
// id: "chalk",
// name: "粉笔",
// description: "模拟粉笔效果,有颗粒感和不连续性",
// category: "特效笔刷",
// ...options,
// });
// }
create() {
this.brush = new fabric.PencilBrush(this.canvas);
this.configure(this.brush, this.options);
// create() {
// this.brush = new fabric.PencilBrush(this.canvas);
// this.configure(this.brush, this.options);
// 自定义绘画方法来模拟粉笔效果
const originalOnMouseMove = this.brush.onMouseMove;
this.brush.onMouseMove = function (pointer, options) {
// 随机调整坐标位置,增加粉笔质感
const jitter = 2;
pointer.x += (Math.random() - 0.5) * jitter;
pointer.y += (Math.random() - 0.5) * jitter;
// // 自定义绘画方法来模拟粉笔效果
// const originalOnMouseMove = this.brush.onMouseMove;
// this.brush.onMouseMove = function (pointer, options) {
// // 随机调整坐标位置,增加粉笔质感
// const jitter = 2;
// pointer.x += (Math.random() - 0.5) * jitter;
// pointer.y += (Math.random() - 0.5) * jitter;
// 调用原始方法
originalOnMouseMove.call(this, pointer, options);
};
// // 调用原始方法
// originalOnMouseMove.call(this, pointer, options);
// };
return this.brush;
}
// return this.brush;
// }
configure(brush, options = {}) {
super.configure(brush, options);
// configure(brush, options = {}) {
// super.configure(brush, options);
// 粉笔特有的设置
brush.strokeDashArray = [5, 5]; // 虚线效果
}
}
);
// // 粉笔特有的设置
// brush.strokeDashArray = [5, 5]; // 虚线效果
// }
// }
// );
}
/**
@@ -571,6 +625,148 @@ export class BrushManager {
return success;
}
/**
* 设置依赖管理器
*/
setManagers({ layerManager, commandManager }) {
this.layerManager = layerManager;
this.commandManager = commandManager;
// 初始化橡皮擦状态管理器
if (this.canvas && this.layerManager) {
this.eraserStateManager = new EraserStateManager(
this.canvas,
this.layerManager
);
}
}
/**
* 创建橡皮擦
* @param {Object} options 橡皮擦选项
*/
createEraser(options = {}) {
if (!this.canvas) {
console.error("画布未初始化");
return null;
}
try {
// 直接使用 fabric-with-erasing 库提供的 EraserBrush
this.brush = new fabric.EraserBrush(this.canvas);
// 应用配置
this.configure(this.brush, {
width: this.brushSize.value,
color: this.brushColor.value,
opacity: this.brushOpacity.value,
inverted: options.inverted || false,
...options,
});
// 设置画布为绘图模式
this.canvas.isDrawingMode = true;
this.canvas.freeDrawingBrush = this.brush;
// 绑定橡皮擦事件处理器
// this._bindEraserEvents();
console.log("橡皮擦创建成功");
return this.brush;
} catch (error) {
console.error("创建橡皮擦失败:", error);
return null;
}
}
/**
* 绑定橡皮擦事件处理器
* @private
*/
_bindEraserEvents() {
if (!this.canvas || !this.eraserStateManager) return;
// 监听擦除开始事件
this.canvas.on("erasing:start", this._handleErasingStart.bind(this));
// 监听擦除结束事件
this.canvas.on("erasing:end", this._handleErasingEnd.bind(this));
}
/**
* 解绑橡皮擦事件处理器
* @private
*/
_unbindEraserEvents() {
if (!this.canvas) return;
this.canvas.off("erasing:start", this._handleErasingStart.bind(this));
this.canvas.off("erasing:end", this._handleErasingEnd.bind(this));
}
/**
* 处理擦除开始事件
* @param {Object} e 事件对象
* @private
*/
_handleErasingStart(e) {
console.log("橡皮擦开始擦除");
if (!this.eraserStateManager) return;
// 标记擦除状态
this.isErasingActive = true;
this.currentErasedObjects = [];
// 捕获擦除前的状态
this.eraserStateManager.startErasing();
}
/**
* 处理擦除结束事件
* @param {Object} e 事件对象
* @private
*/
_handleErasingEnd(e) {
console.log("橡皮擦擦除结束", e);
if (!this.eraserStateManager || !this.isErasingActive) return;
// 获取被擦除的对象
const affectedObjects = e.targets || [];
// 创建橡皮擦命令
const eraserCommand = this.eraserStateManager.endErasing(affectedObjects);
// 如果有有效的命令且有命令管理器,执行命令
if (eraserCommand && this.commandManager) {
// 注意:不需要调用 execute(),因为擦除操作已经完成
// 只需要将命令添加到历史记录中以支持撤销
this.commandManager.addToHistory(eraserCommand);
console.log("橡皮擦操作已添加到命令历史");
}
// 重置状态
this.isErasingActive = false;
this.currentErasedObjects = [];
}
/**
* 取消当前擦除操作
*/
cancelErasing() {
if (!this.isErasingActive) return;
if (this.eraserStateManager) {
this.eraserStateManager.cancelErasing();
}
this.isErasingActive = false;
this.currentErasedObjects = [];
console.log("当前擦除操作已取消");
}
/**
* 获取当前笔刷类型
* @returns {String} 当前笔刷类型ID
@@ -646,7 +842,6 @@ export class BrushManager {
return this.canvas.freeDrawingBrush;
}
/**
* 创建橡皮擦
* @returns {Object} 橡皮擦笔刷
@@ -703,9 +898,20 @@ export class BrushManager {
}
/**
* 销毁资源
* 销毁笔刷管理器
*/
dispose() {
// 解绑事件
// this._unbindEraserEvents();
// 取消进行中的擦除操作
this.cancelErasing();
// 清理状态管理器
if (this.eraserStateManager) {
this.eraserStateManager = null;
}
// 销毁当前笔刷
if (this.activeBrush) {
this.activeBrush.destroy();

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { BaseBrush } from "../BaseBrush";
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import texturePresetManager from "../TexturePresetManager";
/**
@@ -71,7 +71,72 @@ export class TextureBrush extends BaseBrush {
// 创建fabric原生纹理笔刷
this.brush = new fabric.PatternBrush(this.canvas);
// 配置笔刷
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
this.brush
);
const self = this; // 保存外部this引用
this.brush._finalizeAndAddPath = function () {
console.log("TextureBrush: _finalizeAndAddPath called");
const ctx = this.canvas.contextTop;
ctx.closePath();
// 应用点简化如果PatternBrush支持
if (this.decimate && this._points) {
this._points = this.decimatePoints(this._points, this.decimate);
}
console.log(
"TextureBrush: points count =",
this._points ? this._points.length : 0
);
// 检查是否有有效的路径数据
if (!this._points || this._points.length < 2) {
// 如果点数不足,直接请求重新渲染
console.log("TextureBrush: Not enough points, skipping");
this.canvas.requestRenderAll();
return;
}
const pathData = this.convertPointsToSVGPath(this._points);
const isEmpty = self._isEmptySVGPath(pathData);
console.log("TextureBrush: isEmpty =", isEmpty);
if (isEmpty) {
// 如果路径为空,直接请求重新渲染
console.log("TextureBrush: Path is empty, skipping");
this.canvas.requestRenderAll();
return;
}
// 先触发事件,模拟原生行为
const path = this.createPath(pathData);
this.canvas.fire("before:path:created", { path: path });
console.log("TextureBrush: Calling convertToImg");
// 调用 convertToImg 方法将绘制内容转换为图片
if (typeof this.convertToImg === "function") {
this.convertToImg();
console.log("TextureBrush: convertToImg called successfully");
} else {
console.warn(
"convertToImg method not found, falling back to original behavior"
);
// 如果没有convertToImg方法回退到原始行为
this.canvas.add(path);
this.canvas.fire("path:created", { path: path });
this.canvas.clearContext(this.canvas.contextTop);
}
// 重置阴影
this._resetShadow();
};
// 配置笔刷基本属性
this.configure(this.brush, this.options);
// 如果有选中的材质,则设置纹理
@@ -221,6 +286,11 @@ export class TextureBrush extends BaseBrush {
canvasTexture.width = width;
canvasTexture.height = height;
// 应用透明度设置
if (this.textureOpacity < 1) {
ctx.globalAlpha = this.textureOpacity;
}
// 绘制前应用旋转
if (this.textureAngle !== 0) {
ctx.save();
@@ -233,26 +303,16 @@ export class TextureBrush extends BaseBrush {
ctx.drawImage(img, 0, 0, width, height);
}
// 应用透明度
if (this.textureOpacity < 1) {
ctx.globalAlpha = this.textureOpacity;
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
}
// 缓存处理后的纹理图像
this._processedTextureCanvas = canvasTexture;
// 创建Pattern对象
const pattern = new fabric.Pattern({
source: canvasTexture,
repeat: this.textureRepeat,
});
// 使用fabric.js 5.x正确的API方式设置PatternBrush
// 直接设置source属性这是PatternBrush的标准用法
this.brush.source = canvasTexture;
// 设置笔刷源纹理
if (typeof this.brush.setSource === "function") {
this.brush.setSource(pattern);
} else if (typeof this.brush.source === "object") {
this.brush.source = pattern;
} else if (typeof this.brush.pattern === "object") {
this.brush.pattern = pattern;
// 通知画布重绘
if (this.canvas && this.canvas.requestRenderAll) {
this.canvas.requestRenderAll();
}
}
@@ -852,4 +912,37 @@ export class TextureBrush extends BaseBrush {
getTextureStats() {
return texturePresetManager.getStats();
}
/**
* 检查 SVG 路径是否为空
* @private
* @param {Array} pathData SVG 路径数据
* @returns {Boolean} 是否为空路径
*/
_isEmptySVGPath(pathData) {
if (!pathData || pathData.length === 0) {
return true;
}
// 检查路径是否只包含移动命令或者是一个点
let hasDrawing = false;
let moveCount = 0;
for (let i = 0; i < pathData.length; i++) {
const command = pathData[i];
if (command[0] === "M") {
moveCount++;
} else if (
command[0] === "L" ||
command[0] === "Q" ||
command[0] === "C"
) {
hasDrawing = true;
break;
}
}
// 如果只有移动命令且超过1个或者没有绘制命令则认为是空路径
return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount);
}
}

View File

@@ -576,7 +576,7 @@ export class CanvasEventManager {
}
/**
* 合并图层中的对象为图像以提高性能
* 合并图层中的对象为以提高性能
* @param {Object} options 合并选项
* @param {fabric.Image} options.fabricImage 新的图像对象
* @param {Object} options.activeLayer 当前活动图层
@@ -594,7 +594,6 @@ export class CanvasEventManager {
console.warn("合并对象失败:没有活动图层");
return;
}
// 验证是否需要合并
const hasExistingObjects =
Array.isArray(activeLayer.fabricObjects) &&

View File

@@ -4,6 +4,7 @@
*/
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
import { LiquifyCPUManager } from "./LiquifyCPUManager";
import { LayerType } from "../../utils/layerHelper";
export class EnhancedLiquifyManager {
/**
@@ -313,16 +314,40 @@ export class EnhancedLiquifyManager {
if (param in this.params) {
this.params[param] = value;
// 同步更新当前渲染器
if (this.activeRenderer) {
// 同步更新当前渲染器 - 关键修复:确保参数正确传递
if (
this.activeRenderer &&
typeof this.activeRenderer.setParam === "function"
) {
console.log(`EnhancedLiquifyManager 设置参数: ${param}=${value}`);
this.activeRenderer.setParam(param, value);
} else {
console.warn(
`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`
);
}
return true;
}
console.warn(`EnhancedLiquifyManager: 无效参数 ${param}`);
return false;
}
/**
* 批量设置参数
* @param {Object} params 参数对象
*/
setParams(params) {
console.log("EnhancedLiquifyManager 批量设置参数:", params);
if (params && typeof params === "object") {
Object.entries(params).forEach(([key, value]) => {
this.setParam(key, value);
});
}
}
/**
* 获取当前参数
* @returns {Object} 当前参数对象
@@ -348,6 +373,36 @@ export class EnhancedLiquifyManager {
}
}
/**
* 开始液化操作(记录初始点)
* @param {Number} x 初始X坐标
* @param {Number} y 初始Y坐标
*/
startLiquifyOperation(x, y) {
if (
this.activeRenderer &&
typeof this.activeRenderer.startDeformation === "function"
) {
this.activeRenderer.startDeformation(x, y);
}
console.log(
`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`
);
}
/**
* 结束液化操作
*/
endLiquifyOperation() {
if (
this.activeRenderer &&
typeof this.activeRenderer.endDeformation === "function"
) {
this.activeRenderer.endDeformation();
}
console.log(`结束液化操作,渲染模式=${this.renderMode}`);
}
/**
* 应用液化变形
* @param {Object} target 目标对象
@@ -468,7 +523,9 @@ export class EnhancedLiquifyManager {
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
}
}
setRealtimeUpdater(realtimeUpdater) {
this.realtimeUpdater = realtimeUpdater;
}
/**
* 重置液化操作
* @returns {ImageData} 重置后的图像数据
@@ -519,7 +576,7 @@ export class EnhancedLiquifyManager {
// 检查图层是否为空
let objectsToCheck = [];
if (layer.isBackground || layer.type === "background") {
if (layer.isBackground || layer.type === "background" || layer.isFixed) {
// 背景图层使用 fabricObject (单数)
if (layer.fabricObject) {
objectsToCheck = [layer.fabricObject];
@@ -548,7 +605,10 @@ export class EnhancedLiquifyManager {
objectsToCheck[0].type === "rasterized-layer");
// 检查是否为组
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
const isGroup =
objectsToCheck.some((obj) => obj.type === "group") ||
layer.type === LayerType.GROUP ||
layer.children?.length > 0;
// 如果不是单一图像,需要栅格化
const needsRasterization = !isImage || isGroup;
@@ -572,26 +632,34 @@ export class EnhancedLiquifyManager {
async _getImageData(fabricObject) {
return new Promise((resolve, reject) => {
try {
// 创建临时canvas
// 创建临时canvas - 关键修复使用原始图像尺寸不考虑fabric对象的缩放
const tempCanvas = document.createElement("canvas");
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
// 使用图像的原始尺寸,而不是缩放后的尺寸
tempCanvas.width = fabricObject.width;
tempCanvas.height = fabricObject.height;
const tempCtx = tempCanvas.getContext("2d");
// 如果对象有图像元素
if (fabricObject._element) {
// 绘制原始尺寸的图像
tempCtx.drawImage(
fabricObject._element,
0,
0,
tempCanvas.width,
tempCanvas.height
fabricObject.width,
fabricObject.height
);
} else if (fabricObject.getSrc) {
// 通过URL创建图像
const img = new Image();
img.onload = () => {
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
tempCtx.drawImage(
img,
0,
0,
fabricObject.width,
fabricObject.height
);
const imageData = tempCtx.getImageData(
0,
0,
@@ -615,6 +683,13 @@ export class EnhancedLiquifyManager {
tempCanvas.width,
tempCanvas.height
);
console.log(
`获取图像数据: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
`图像数据尺寸=${imageData.width}x${imageData.height}`
);
resolve(imageData);
} catch (error) {
reject(error);

View File

@@ -0,0 +1,451 @@
/**
* 混合液化管理器 - 根据模式智能选择算法
*/
export class HybridLiquifyManager {
constructor(options = {}) {
this.config = {
gridSize: options.gridSize || 16,
maxStrength: options.maxStrength || 200,
smoothingIterations: options.smoothingIterations || 1,
relaxFactor: options.relaxFactor || 0.1,
};
this.params = {
size: 80,
pressure: 0.8,
distortion: 0,
power: 0.8,
};
this.modes = {
PUSH: "push",
CLOCKWISE: "clockwise",
COUNTERCLOCKWISE: "counterclockwise",
PINCH: "pinch",
EXPAND: "expand",
CRYSTAL: "crystal",
EDGE: "edge",
RECONSTRUCT: "reconstruct",
};
// 定义哪些模式使用像素算法
this.pixelModes = new Set([
this.modes.CLOCKWISE,
this.modes.COUNTERCLOCKWISE,
this.modes.CRYSTAL,
this.modes.EDGE,
]);
// 定义哪些模式使用网格算法
this.meshModes = new Set([
this.modes.PUSH,
this.modes.PINCH,
this.modes.EXPAND,
this.modes.RECONSTRUCT,
]);
this.currentMode = this.modes.PUSH;
this.originalImageData = null;
this.currentImageData = null;
this.mesh = null;
this.initialized = false;
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d");
// 持续按压相关状态
this.pressStartTime = 0;
this.pressDuration = 0;
this.accumulatedRotation = 0;
this.accumulatedScale = 0;
this.isHolding = false;
this.continuousTimer = null;
// 鼠标状态
this.initialMouseX = 0;
this.initialMouseY = 0;
this.currentMouseX = 0;
this.currentMouseY = 0;
this.isDragging = false;
this.dragDistance = 0;
}
// ...existing initialization methods...
/**
* 应用液化变形 - 智能选择算法
*/
applyDeformation(x, y) {
if (!this.initialized || !this.originalImageData) {
return this.currentImageData;
}
// 更新鼠标位置
this.currentMouseX = x;
this.currentMouseY = y;
const { size, pressure, power } = this.params;
const radius = size;
const strength = pressure * power;
// 根据模式选择算法
if (this.pixelModes.has(this.currentMode)) {
return this._applyPixelDeformation(x, y, radius, strength);
} else if (this.meshModes.has(this.currentMode)) {
return this._applyMeshDeformation(x, y, radius, strength);
}
return this.currentImageData;
}
/**
* 像素级液化算法 - 适用于旋转、水晶、边缘模式
*/
_applyPixelDeformation(centerX, centerY, radius, strength) {
const data = this.currentImageData.data;
const width = this.currentImageData.width;
const height = this.currentImageData.height;
const tempData = new Uint8ClampedArray(data);
const processRadius = Math.min(radius, Math.min(width, height) / 2);
// 计算影响区域边界
const minX = Math.max(0, Math.floor(centerX - processRadius));
const maxX = Math.min(width, Math.ceil(centerX + processRadius));
const minY = Math.max(0, Math.floor(centerY - processRadius));
const maxY = Math.min(height, Math.ceil(centerY + processRadius));
switch (this.currentMode) {
case this.modes.CLOCKWISE:
case this.modes.COUNTERCLOCKWISE:
this._applyPixelRotation(
tempData,
data,
width,
height,
centerX,
centerY,
processRadius,
strength,
this.currentMode === this.modes.CLOCKWISE
);
break;
case this.modes.CRYSTAL:
this._applyPixelCrystal(
tempData,
data,
width,
height,
centerX,
centerY,
processRadius,
strength
);
break;
case this.modes.EDGE:
this._applyPixelEdge(
tempData,
data,
width,
height,
centerX,
centerY,
processRadius,
strength
);
break;
}
return this.currentImageData;
}
/**
* 像素级旋转算法
*/
_applyPixelRotation(
srcData,
dstData,
width,
height,
centerX,
centerY,
radius,
strength,
clockwise
) {
// 计算旋转角度
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
const baseRotationSpeed = 0.015;
const rotationAngle =
(clockwise ? 1 : -1) *
baseRotationSpeed *
strength *
(1.0 + timeFactor * 0.3);
this.accumulatedRotation += rotationAngle;
const minX = Math.max(0, Math.floor(centerX - radius));
const maxX = Math.min(width, Math.ceil(centerX + radius));
const minY = Math.max(0, Math.floor(centerY - radius));
const maxY = Math.min(height, Math.ceil(centerY + radius));
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius && distance > 0.1) {
// 距离衰减:内圈快,外圈慢
const normalizedDistance = distance / radius;
const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减
// 计算旋转后的源位置
const angle = Math.atan2(dy, dx);
const newAngle = angle + this.accumulatedRotation * falloff;
const sourceX = centerX + Math.cos(newAngle) * distance;
const sourceY = centerY + Math.sin(newAngle) * distance;
// 双线性插值采样
const color = this._bilinearSample(
srcData,
width,
height,
sourceX,
sourceY
);
if (color) {
const targetIdx = (y * width + x) * 4;
dstData[targetIdx] = color[0];
dstData[targetIdx + 1] = color[1];
dstData[targetIdx + 2] = color[2];
dstData[targetIdx + 3] = color[3];
}
}
}
}
}
/**
* 像素级水晶效果
*/
_applyPixelCrystal(
srcData,
dstData,
width,
height,
centerX,
centerY,
radius,
strength
) {
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
const distortionStrength = strength * (1.0 + timeFactor * 0.5);
const minX = Math.max(0, Math.floor(centerX - radius));
const maxX = Math.min(width, Math.ceil(centerX + radius));
const minY = Math.max(0, Math.floor(centerY - radius));
const maxY = Math.min(height, Math.ceil(centerY + radius));
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius && distance > 0.1) {
const normalizedDistance = distance / radius;
const falloff = 1 - normalizedDistance * normalizedDistance;
const angle = Math.atan2(dy, dx);
const crystalRadius = normalizedDistance;
// 多层波浪扭曲
const wave1 = Math.sin(angle * 8 + this.pressDuration * 0.005) * 0.6;
const wave2 = Math.cos(angle * 12 + this.pressDuration * 0.003) * 0.4;
const waveAngle =
angle + (wave1 + wave2) * distortionStrength * falloff;
const radialMod =
1 +
Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) *
0.3;
const modDistance = distance * radialMod;
const sourceX = centerX + Math.cos(waveAngle) * modDistance;
const sourceY = centerY + Math.sin(waveAngle) * modDistance;
const color = this._bilinearSample(
srcData,
width,
height,
sourceX,
sourceY
);
if (color) {
const targetIdx = (y * width + x) * 4;
const factor = falloff * distortionStrength * 0.7;
// 混合原始颜色和扭曲颜色
const originalIdx = (y * width + x) * 4;
dstData[targetIdx] = Math.round(
srcData[originalIdx] * (1 - factor) + color[0] * factor
);
dstData[targetIdx + 1] = Math.round(
srcData[originalIdx + 1] * (1 - factor) + color[1] * factor
);
dstData[targetIdx + 2] = Math.round(
srcData[originalIdx + 2] * (1 - factor) + color[2] * factor
);
dstData[targetIdx + 3] = srcData[originalIdx + 3];
}
}
}
}
}
/**
* 像素级边缘效果
*/
_applyPixelEdge(
srcData,
dstData,
width,
height,
centerX,
centerY,
radius,
strength
) {
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
const edgeStrength = strength * (1.0 + timeFactor * 0.4);
const minX = Math.max(0, Math.floor(centerX - radius));
const maxX = Math.min(width, Math.ceil(centerX + radius));
const minY = Math.max(0, Math.floor(centerY - radius));
const maxY = Math.min(height, Math.ceil(centerY + radius));
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius && distance > 0.1) {
const normalizedDistance = distance / radius;
const falloff = 1 - normalizedDistance * normalizedDistance;
const angle = Math.atan2(dy, dx);
const edgeRadius = normalizedDistance;
const edgeWave =
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
Math.cos(angle * 6 + this.pressDuration * 0.002);
const perpAngle = angle + Math.PI / 2;
const edgeFactor = edgeWave * falloff * edgeStrength * 0.5;
const offsetX = Math.cos(perpAngle) * edgeFactor;
const offsetY = Math.sin(perpAngle) * edgeFactor;
const sourceX = x + offsetX;
const sourceY = y + offsetY;
const color = this._bilinearSample(
srcData,
width,
height,
sourceX,
sourceY
);
if (color) {
const targetIdx = (y * width + x) * 4;
dstData[targetIdx] = color[0];
dstData[targetIdx + 1] = color[1];
dstData[targetIdx + 2] = color[2];
dstData[targetIdx + 3] = color[3];
}
}
}
}
}
/**
* 双线性插值采样
*/
_bilinearSample(data, width, height, x, y) {
if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) {
return null;
}
const x1 = Math.floor(x);
const y1 = Math.floor(y);
const x2 = x1 + 1;
const y2 = y1 + 1;
const fx = x - x1;
const fy = y - y1;
const getPixel = (px, py) => {
const idx = (py * width + px) * 4;
return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
};
const p1 = getPixel(x1, y1);
const p2 = getPixel(x2, y1);
const p3 = getPixel(x1, y2);
const p4 = getPixel(x2, y2);
return [
Math.round(
(1 - fx) * (1 - fy) * p1[0] +
fx * (1 - fy) * p2[0] +
(1 - fx) * fy * p3[0] +
fx * fy * p4[0]
),
Math.round(
(1 - fx) * (1 - fy) * p1[1] +
fx * (1 - fy) * p2[1] +
(1 - fx) * fy * p3[1] +
fx * fy * p4[1]
),
Math.round(
(1 - fx) * (1 - fy) * p1[2] +
fx * (1 - fy) * p2[2] +
(1 - fx) * fy * p3[2] +
fx * fy * p4[2]
),
Math.round(
(1 - fx) * (1 - fy) * p1[3] +
fx * (1 - fy) * p2[3] +
(1 - fx) * fy * p3[3] +
fx * fy * p4[3]
),
];
}
/**
* 网格液化算法 - 适用于推拉、捏合、展开模式
*/
_applyMeshDeformation(x, y, radius, strength) {
if (!this.mesh) return this.currentImageData;
// 使用现有的网格算法处理推拉、捏合、展开
const mode = this.currentMode;
const { distortion } = this.params;
this._applyDeformation(x, y, radius, strength, mode, distortion);
if (this.config.smoothingIterations > 0) {
this._smoothMesh();
}
return this._applyMeshToImage();
}
// ...existing mesh methods...
}

View File

@@ -38,7 +38,7 @@ export class LiquifyManager {
meshResolution: options.meshResolution || 64,
// 根据环境选择合适的渲染模式
forceCPU: true, // 默认不强制使用CPU
forceWebGL: false, // 优先使用WebGL模式
forceWebGL: true, // 优先使用WebGL模式
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
layerManager: options.layerManager || null,
canvas: options.canvas || null,
@@ -87,9 +87,19 @@ export class LiquifyManager {
* @param {Number} value 参数值
*/
setParam(param, value) {
console.log(`LiquifyManager 设置参数: ${param}=${value}`);
return this.enhancedManager.setParam(param, value);
}
/**
* 批量设置参数
* @param {Object} params 参数对象
*/
setParams(params) {
console.log("LiquifyManager 批量设置参数:", params);
return this.enhancedManager.setParams(params);
}
/**
* 获取当前参数
* @returns {Object} 当前参数对象
@@ -120,21 +130,14 @@ export class LiquifyManager {
return null;
}
// 确保设置正确的模式和参数
if (mode) {
this.enhancedManager.setMode(mode);
}
console.log(
`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`,
params
);
if (params) {
Object.entries(params).forEach(([key, value]) => {
this.enhancedManager.setParam(key, value);
});
}
// 应用液化变形
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
try {
// 直接调用EnhancedLiquifyManager的applyLiquify方法
// 避免重复设置参数让EnhancedLiquifyManager处理参数设置
const resultData = await this.enhancedManager.applyLiquify(
targetObject,
mode,
@@ -146,6 +149,13 @@ export class LiquifyManager {
// 确保返回结果数据
if (!resultData) {
console.warn("液化变形没有返回结果数据");
} else {
console.log(
"✅ 液化变形成功,返回图像数据尺寸:",
resultData.width,
"x",
resultData.height
);
}
return resultData;
@@ -180,6 +190,22 @@ export class LiquifyManager {
return this.enhancedManager.getStatus();
}
/**
* 开始液化操作(记录初始点)
* @param {Number} x 初始X坐标
* @param {Number} y 初始Y坐标
*/
startLiquifyOperation(x, y) {
return this.enhancedManager.startLiquifyOperation(x, y);
}
/**
* 结束液化操作
*/
endLiquifyOperation() {
return this.enhancedManager.endLiquifyOperation();
}
/**
* 释放资源
*/

View File

@@ -0,0 +1,443 @@
/**
* 液化实时更新器
* 负责高效地更新液化效果到画布上避免频繁创建fabric对象导致的性能问题
*/
export class LiquifyRealTimeUpdater {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.targetObject = null;
this.isUpdating = false;
this.updateQueue = [];
this.lastUpdateTime = 0;
this.currImage = options.currImage || { value: null };
// 配置选项
this.config = {
throttleTime: options.throttleTime || 16, // 60fps
maxQueueSize: options.maxQueueSize || 5,
useDirectUpdate: options.useDirectUpdate !== false, // 默认启用直接更新
imageQuality: options.imageQuality || 1.0, // 图像质量 (0.1-1.0)
skipRenderDuringDrag: options.skipRenderDuringDrag || false, // 拖拽时跳过渲染
};
// 临时canvas用于快速渲染
this.tempCanvas = document.createElement("canvas");
this.tempCtx = this.tempCanvas.getContext("2d");
// 高质量canvas用于最终输出
this.highQualityCanvas = document.createElement("canvas");
this.highQualityCtx = this.highQualityCanvas.getContext("2d");
// 当前缓存的图像数据
this.cachedDataURL = null;
this.pendingImageData = null;
this.renderingScheduled = false;
// 优化Canvas画布渲染设置
this.canvas.renderOnAddRemove = false; // 禁用自动渲染
this.canvas.skipOffscreen = true; // 跳过离屏元素渲染
}
/**
* 设置目标对象
* @param {Object} fabricObject fabric图像对象
*/
setTargetObject(fabricObject) {
this.targetObject = fabricObject;
if (fabricObject && fabricObject._element) {
// 设置临时canvas尺寸
this.tempCanvas.width = fabricObject.width;
this.tempCanvas.height = fabricObject.height;
// 设置高质量canvas尺寸
this.highQualityCanvas.width = fabricObject.width;
this.highQualityCanvas.height = fabricObject.height;
// 配置高质量渲染上下文
this.highQualityCtx.imageSmoothingEnabled = true;
this.highQualityCtx.imageSmoothingQuality = "high";
// 配置临时canvas上下文快速渲染
this.tempCtx.imageSmoothingEnabled = false; // 快速模式关闭平滑
}
}
/**
* 实时更新图像数据到画布
* @param {ImageData} imageData 新的图像数据
* @param {Boolean} isDrawing 是否正在绘制(拖拽过程中)
* @returns {Promise} 更新完成的Promise
*/
async updateImage(imageData, isDrawing = false) {
if (!this.targetObject || !imageData) {
return;
}
// 节流控制
const now = Date.now();
if (now - this.lastUpdateTime < this.config.throttleTime && isDrawing) {
// 在绘制过程中进行节流,缓存最新的图像数据
this.pendingImageData = imageData;
return;
}
this.lastUpdateTime = now;
if (isDrawing && this.config.useDirectUpdate) {
// 拖拽过程中使用快速更新(降低质量以提高性能)
this._fastUpdate(imageData);
} else {
// 拖拽结束后使用完整更新(最高质量)
await this._fullUpdate(imageData);
}
}
/**
* 智能图像质量更新
* 根据图像尺寸和设备性能动态调整质量
* @param {ImageData} imageData 图像数据
* @param {Boolean} isDrawing 是否正在绘制
* @private
*/
_getOptimalQuality(imageData, isDrawing) {
const pixelCount = imageData.width * imageData.height;
if (isDrawing) {
// 拖拽时根据图像大小调整质量
if (pixelCount > 1000000) {
// 大于1M像素
return 0.7;
} else if (pixelCount > 500000) {
// 大于500K像素
return 0.8;
} else {
return 0.9;
}
} else {
// 拖拽结束时始终使用最高质量
return 1.0;
}
}
/**
* 快速更新 - 直接修改现有对象的图像源
* @param {ImageData} imageData 图像数据
* @private
*/
_fastUpdate(imageData) {
if (!this.targetObject || !this.targetObject._element) {
return;
}
try {
// 将ImageData渲染到临时canvas快速模式
this.tempCtx.putImageData(imageData, 0, 0);
// 获取智能质量设置
const quality = this._getOptimalQuality(imageData, true);
// 直接更新fabric对象的图像源使用PNG格式保持质量
const targetElement = this.targetObject._element;
// 方案1: 直接设置src属性最高性能
const dataURL = this.tempCanvas.toDataURL("image/png", quality);
if (targetElement.src !== dataURL) {
targetElement.src = dataURL;
// 关键优化直接设置fabric对象为脏状态但不立即渲染
// this.targetObject.dirty = false; // 标记为不需要立即渲染
// this.canvas.renderOnAddRemove = true; // 恢复自动渲染
// this.renderingScheduled = false; // 重置渲染调度状态
this?.scheduleRender?.(); // 调度一次渲染
// 使用requestAnimationFrame进行批量渲染优化
// if (!this.renderingScheduled && !this.config.skipRenderDuringDrag) {
// this.renderingScheduled = true;
// requestIdleCallback(() => {
// this.canvas.renderAll();
// this.renderingScheduled = false;
// });
// }
} else {
console.warn(
"=================快速更新液化效果时,图像数据未变化,跳过更新"
);
}
} catch (error) {
console.error("快速更新液化效果失败:", error);
}
}
getImageData(imageData) {
// 使用高质量canvas进行最终渲染
this.highQualityCtx.putImageData(imageData, 0, 0);
// 生成高质量DataURLPNG格式最大质量
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
return dataURL;
}
/**
* 完整更新 - 创建新的fabric对象
* @param {ImageData} imageData 图像数据
* @private
*/
async _fullUpdate(imageData) {
return new Promise((resolve, reject) => {
try {
// 使用高质量canvas进行最终渲染
this.highQualityCtx.putImageData(imageData, 0, 0);
// 生成高质量DataURLPNG格式最大质量
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
// 如果DataURL没有变化跳过更新
if (this.cachedDataURL === dataURL) {
resolve();
return;
}
this.cachedDataURL = dataURL;
// 创建新的fabric图像对象保持最高质量
fabric.Image.fromURL(
dataURL,
(newImg) => {
try {
if (!this.targetObject) {
console.warn("目标对象为空,跳过更新");
resolve();
return;
}
// 保存原对象信息用于智能查找
const originalObjId = this.targetObject.id;
const originalObjLayerId = this.targetObject.layerId;
// 保留原对象的所有变换属性
const originalObj = this.targetObject;
newImg.set({
left: originalObj.left,
top: originalObj.top,
scaleX: originalObj.scaleX,
scaleY: originalObj.scaleY,
angle: originalObj.angle,
flipX: originalObj.flipX,
flipY: originalObj.flipY,
opacity: originalObj.opacity,
originX: originalObj.originX,
originY: originalObj.originY,
id: originalObj.id,
name: originalObj.name,
layerId: originalObj.layerId,
selected: false,
evented: originalObj.evented,
});
// 临时禁用画布自动渲染
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
this.canvas.renderOnAddRemove = false;
// 智能查找和替换canvas上的对象
const allObjects = this.canvas.getObjects();
let targetIndex = allObjects.indexOf(originalObj);
// 如果直接查找失败尝试通过ID查找
if (targetIndex === -1 && originalObjId) {
targetIndex = allObjects.findIndex(
(obj) => obj.id === originalObjId
);
if (targetIndex !== -1) {
console.log(`通过ID找到目标对象: ${originalObjId}`);
// 更新目标对象引用
this.targetObject = allObjects[targetIndex];
}
}
// 如果通过ID查找仍然失败尝试通过图层ID查找
if (targetIndex === -1 && originalObjLayerId) {
targetIndex = allObjects.findIndex(
(obj) => obj.layerId === originalObjLayerId
);
if (targetIndex !== -1) {
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
// 更新目标对象引用
this.targetObject = allObjects[targetIndex];
}
}
if (targetIndex !== -1) {
// 找到目标对象,执行替换
this.canvas.remove(this.targetObject);
this.canvas.insertAt(newImg, targetIndex);
// 恢复自动渲染设置
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
// 更新目标对象引用
this.targetObject = newImg;
// 一次性重新渲染画布
this.canvas.renderAll();
console.log(`✅ 液化对象更新成功,位置: ${targetIndex}`);
resolve(newImg);
} else {
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
console.warn(
"在画布中找不到目标对象,可能已被其他操作移除或替换"
);
// 恢复自动渲染设置
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
// 尝试添加新对象到画布末尾
this.canvas.add(newImg);
this.targetObject = newImg;
this.canvas.renderAll();
console.log("🔄 已将新对象添加到画布末尾");
resolve(newImg);
}
} catch (error) {
// 恢复自动渲染设置
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
console.error("更新fabric对象时出错:", error);
reject(error);
}
},
{ crossOrigin: "anonymous" }
); // 确保跨域支持
} catch (error) {
console.error("完整更新过程出错:", error);
reject(error);
}
});
}
/**
* 处理待处理的图像数据
* 在拖拽结束后调用,处理可能积压的更新
*/
async processPendingUpdates() {
if (this.pendingImageData && !this.isUpdating) {
this.isUpdating = true;
try {
await this._fullUpdate(this.pendingImageData);
this.pendingImageData = null;
} catch (error) {
console.error("处理待处理更新失败:", error);
} finally {
this.isUpdating = false;
}
}
}
/**
* 清理资源
*/
dispose() {
this.targetObject = null;
this.cachedDataURL = null;
this.pendingImageData = null;
this.updateQueue.length = 0;
// 清理临时canvas
if (this.tempCanvas) {
this.tempCanvas.width = 0;
this.tempCanvas.height = 0;
this.tempCanvas = null;
this.tempCtx = null;
}
}
/**
* 获取当前目标对象
* @returns {Object} 当前的fabric对象
*/
getTargetObject() {
return this.targetObject;
}
/**
* 强制进行完整更新
* @param {ImageData} imageData 图像数据
*/
async forceFullUpdate(imageData) {
return this._fullUpdate(imageData);
}
/**
* 启用拖拽模式 - 暂停渲染以提高性能
*/
enableDragMode() {
this.config.skipRenderDuringDrag = true;
this.canvas.renderOnAddRemove = false;
console.log("🚀 启用拖拽优化模式");
}
/**
* 禁用拖拽模式 - 恢复正常渲染
*/
disableDragMode() {
this.config.skipRenderDuringDrag = false;
this.canvas.renderOnAddRemove = true;
// 执行一次完整渲染
this.canvas.renderAll();
console.log("✅ 恢复正常渲染模式");
}
/**
* 获取当前目标对象
*/
getTargetObject() {
return this.targetObject;
}
/**
* 设置图像质量
* @param {Number} quality 质量值 (0.1-1.0)
*/
setImageQuality(quality) {
this.config.imageQuality = Math.max(0.1, Math.min(1.0, quality));
}
/**
* 优化的批量渲染方法
*/
scheduleRender() {
if (!this.renderingScheduled) {
this.renderingScheduled = true;
requestAnimationFrame(() => {
this.canvas.renderAll();
this.renderingScheduled = false;
});
}
}
/**
* 清理资源
*/
dispose() {
// 恢复canvas设置
this.canvas.renderOnAddRemove = true;
// 清理缓存
this.cachedDataURL = null;
this.pendingImageData = null;
// 清理canvas
if (this.tempCanvas) {
this.tempCanvas.width = 0;
this.tempCanvas.height = 0;
}
if (this.highQualityCanvas) {
this.highQualityCanvas.width = 0;
this.highQualityCanvas.height = 0;
}
console.log("🧹 液化实时更新器资源已清理");
}
}

View File

@@ -0,0 +1,431 @@
/**
* 液化面板状态管理器
* 负责管理液化操作的状态、性能优化和用户反馈
*/
export class LiquifyStateManager {
constructor(canvas, realtimeUpdater) {
this.canvas = canvas;
this.realtimeUpdater = realtimeUpdater;
// 状态管理
this.isOperating = false;
this.isDragging = false;
this.operationCount = 0;
this.startTime = null;
// 性能监控
this.performanceMetrics = {
totalOperations: 0,
totalTime: 0,
averageTime: 0,
maxTime: 0,
minTime: Infinity,
lastOperationTime: 0,
};
// 用户反馈
this.feedbackEnabled = true;
this.cursorCache = new Map();
// 设备性能检测
this.devicePerformance = this._detectDevicePerformance();
console.log(
"🎯 液化状态管理器已初始化,设备性能等级:",
this.devicePerformance
);
}
/**
* 开始液化操作
*/
startOperation() {
if (this.isOperating) return;
this.isOperating = true;
this.startTime = performance.now();
// 根据设备性能调整设置
this._adjustPerformanceSettings();
// 显示操作反馈
this._showOperationFeedback();
console.log("🚀 开始液化操作");
}
/**
* 开始拖拽
*/
startDrag() {
if (this.isDragging) return;
this.isDragging = true;
// 优化拖拽性能
this.realtimeUpdater?.enableDragMode();
// 更新鼠标样式
this._updateCursor("liquifying");
// 禁用不必要的画布功能
this._disableCanvasFeatures();
console.log("🖱️ 开始拖拽操作");
}
/**
* 结束拖拽
*/
async endDrag() {
if (!this.isDragging) return;
// 恢复鼠标样式
this._updateCursor("default");
// 恢复画布功能
this._enableCanvasFeatures();
// 处理待处理的更新
if (this.realtimeUpdater) {
try {
await this.realtimeUpdater.processPendingUpdates();
} catch (error) {
console.error("处理待处理更新失败:", error);
} finally {
this.isDragging = false;
// 恢复正常模式
this.realtimeUpdater?.disableDragMode();
// 结束液化操作 添加结果到命令中 更新当前激活图层对象
}
}
console.log("✅ 结束拖拽操作");
}
/**
* 结束液化操作
*/
endOperation() {
if (!this.isOperating) return;
const operationTime = performance.now() - this.startTime;
this._updatePerformanceMetrics(operationTime);
this.isOperating = false;
this.operationCount++;
// 隐藏操作反馈
this._hideOperationFeedback();
console.log(`⏱️ 液化操作完成,耗时: ${operationTime.toFixed(2)}ms`);
}
/**
* 记录单次变形操作
*/
recordDeformation(operationTime) {
this.performanceMetrics.totalOperations++;
this.performanceMetrics.totalTime += operationTime;
this.performanceMetrics.averageTime =
this.performanceMetrics.totalTime /
this.performanceMetrics.totalOperations;
this.performanceMetrics.maxTime = Math.max(
this.performanceMetrics.maxTime,
operationTime
);
this.performanceMetrics.minTime = Math.min(
this.performanceMetrics.minTime,
operationTime
);
this.performanceMetrics.lastOperationTime = operationTime;
}
/**
* 记录操作性能指标
* @param {Object} metrics 性能指标对象
*/
recordOperationMetrics(metrics) {
const {
operationTime,
operationType,
mode,
coordinates,
imageSize,
renderMode,
isRealTime,
} = metrics;
// 记录基础性能数据
this.recordDeformation(operationTime);
// 记录详细操作信息
this.performanceMetrics.lastOperation = {
type: operationType,
mode,
coordinates,
imageSize,
renderMode,
isRealTime,
timestamp: Date.now(),
};
// 根据性能数据动态调整设置
this._adaptivePerformanceOptimization(operationTime);
console.log(
`📊 记录性能指标: ${operationType}/${mode}, 耗时: ${operationTime.toFixed(
2
)}ms, 渲染模式: ${renderMode}`
);
}
/**
* 自适应性能优化
* @param {Number} operationTime 操作耗时
* @private
*/
_adaptivePerformanceOptimization(operationTime) {
if (!this.realtimeUpdater) return;
// 如果操作耗时过长,动态降低质量或增加节流时间
if (operationTime > 50 && this.devicePerformance !== "high") {
// 降低图像质量
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
if (currentQuality > 0.7) {
this.realtimeUpdater.setImageQuality(
Math.max(0.7, currentQuality - 0.1)
);
console.log("⚡ 自动降低图像质量以提升性能");
}
// 增加节流时间
if (this.realtimeUpdater.config.throttleTime < 33) {
this.realtimeUpdater.config.throttleTime = Math.min(
33,
this.realtimeUpdater.config.throttleTime + 8
);
console.log("⏱️ 自动增加节流时间以提升性能");
}
}
// 如果性能很好,可以适当提高质量
if (operationTime < 20 && this.devicePerformance === "high") {
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
if (currentQuality < 1.0) {
this.realtimeUpdater.setImageQuality(
Math.min(1.0, currentQuality + 0.05)
);
}
}
}
/**
* 获取性能报告
*/
getPerformanceReport() {
return {
...this.performanceMetrics,
devicePerformance: this.devicePerformance,
fps: this._calculateFPS(),
recommendations: this._generateRecommendations(),
};
}
/**
* 设置用户反馈
*/
setFeedbackEnabled(enabled) {
this.feedbackEnabled = enabled;
}
/**
* 清理资源
*/
dispose() {
this._enableCanvasFeatures();
this._updateCursor("default");
this.cursorCache.clear();
console.log("🧹 液化状态管理器已清理");
}
// === 私有方法 ===
/**
* 检测设备性能
*/
_detectDevicePerformance() {
// 检测硬件并发数
const cores = navigator.hardwareConcurrency || 4;
// 检测内存
const memory = navigator.deviceMemory || 4;
// 检测连接类型
const connection = navigator.connection;
const effectiveType = connection?.effectiveType || "4g";
// 简单的性能评分算法
let score = 0;
score += cores * 10;
score += memory * 5;
if (effectiveType === "4g") score += 10;
else if (effectiveType === "3g") score += 5;
// 性能等级
if (score >= 70) return "high";
if (score >= 40) return "medium";
return "low";
}
/**
* 根据设备性能调整设置
*/
_adjustPerformanceSettings() {
if (!this.realtimeUpdater) return;
switch (this.devicePerformance) {
case "high":
this.realtimeUpdater.setImageQuality(1.0);
this.realtimeUpdater.config.throttleTime = 8; // 120fps
break;
case "medium":
this.realtimeUpdater.setImageQuality(0.9);
this.realtimeUpdater.config.throttleTime = 16; // 60fps
break;
case "low":
this.realtimeUpdater.setImageQuality(0.8);
this.realtimeUpdater.config.throttleTime = 33; // 30fps
break;
}
}
/**
* 显示操作反馈
*/
_showOperationFeedback() {
if (!this.feedbackEnabled) return;
// 添加视觉反馈(可以是加载动画、进度条等)
document.body.style.cursor = "wait";
}
/**
* 隐藏操作反馈
*/
_hideOperationFeedback() {
if (!this.feedbackEnabled) return;
document.body.style.cursor = "default";
}
/**
* 更新鼠标样式
*/
_updateCursor(type) {
if (!this.feedbackEnabled) return;
const cursors = {
default: "default",
liquifying: "crosshair",
wait: "wait",
"not-allowed": "not-allowed",
};
const cursor = cursors[type] || "default";
if (this.canvas && this.canvas.upperCanvasEl) {
this.canvas.upperCanvasEl.style.cursor = cursor;
}
}
/**
* 禁用画布功能以提高性能
*/
_disableCanvasFeatures() {
if (!this.canvas) return;
// 保存原始设置
this._originalSettings = {
renderOnAddRemove: this.canvas.renderOnAddRemove,
skipOffscreen: this.canvas.skipOffscreen,
enableRetinaScaling: this.canvas.enableRetinaScaling,
};
// 应用性能优化设置
this.canvas.renderOnAddRemove = false;
this.canvas.skipOffscreen = true;
// 低性能设备关闭高分辨率支持
if (this.devicePerformance === "low") {
this.canvas.enableRetinaScaling = false;
}
}
/**
* 恢复画布功能
*/
_enableCanvasFeatures() {
if (!this.canvas || !this._originalSettings) return;
// 恢复原始设置
this.canvas.renderOnAddRemove = this._originalSettings.renderOnAddRemove;
this.canvas.skipOffscreen = this._originalSettings.skipOffscreen;
this.canvas.enableRetinaScaling =
this._originalSettings.enableRetinaScaling;
this._originalSettings = null;
}
/**
* 更新性能指标
*/
_updatePerformanceMetrics(operationTime) {
this.performanceMetrics.totalTime += operationTime;
this.performanceMetrics.averageTime =
this.performanceMetrics.totalTime / (this.operationCount + 1);
this.performanceMetrics.maxTime = Math.max(
this.performanceMetrics.maxTime,
operationTime
);
this.performanceMetrics.minTime = Math.min(
this.performanceMetrics.minTime,
operationTime
);
}
/**
* 计算FPS
*/
_calculateFPS() {
if (this.performanceMetrics.averageTime === 0) return 0;
return Math.round(1000 / this.performanceMetrics.averageTime);
}
/**
* 生成性能建议
*/
_generateRecommendations() {
const recommendations = [];
if (this.performanceMetrics.averageTime > 50) {
recommendations.push("操作响应较慢,建议降低图像尺寸或关闭高质量模式");
}
if (this.devicePerformance === "low") {
recommendations.push("检测到低性能设备,已自动启用性能优化模式");
}
const fps = this._calculateFPS();
if (fps < 30) {
recommendations.push("帧率较低,建议减少同时进行的操作或降低液化强度");
}
return recommendations;
}
}

View File

@@ -1,4 +1,4 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
/**
* 小地图管理器类

View File

@@ -1,4 +1,4 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import { generateId } from "../../utils/helper";
import { OperationType } from "../../utils/layerHelper";
import {

View File

@@ -0,0 +1,480 @@
/**
* 液化功能集成测试
* 用于在浏览器环境中测试完整的液化工作流程
*/
import { LiquifyCPUManager } from "../managers/liquify/LiquifyCPUManager.js";
import { LiquifyWebGLManager } from "../managers/liquify/LiquifyWebGLManager.js";
import { LiquifyRealTimeUpdater } from "../managers/liquify/LiquifyRealTimeUpdater.js";
import { EnhancedLiquifyManager } from "../managers/liquify/EnhancedLiquifyManager.js";
// 集成测试结果
let testResults = {
totalTests: 0,
passedTests: 0,
failedTests: 0,
errors: [],
};
/**
* 创建测试图像数据
*/
function createTestImageData(width = 100, height = 100) {
const imageData = new ImageData(width, height);
const data = imageData.data;
// 创建一个简单的渐变图案用于测试
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
data[index] = (x / width) * 255; // Red
data[index + 1] = (y / height) * 255; // Green
data[index + 2] = 128; // Blue
data[index + 3] = 255; // Alpha
}
}
return imageData;
}
/**
* 创建测试Canvas
*/
function createTestCanvas(width = 200, height = 200) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
return canvas;
}
/**
* 创建模拟Fabric对象
*/
function createMockFabricObject(imageData) {
return {
left: 50,
top: 50,
width: imageData.width,
height: imageData.height,
scaleX: 1,
scaleY: 1,
angle: 0,
flipX: false,
flipY: false,
_element: null,
calcTransformMatrix: function () {
return [this.scaleX, 0, 0, this.scaleY, this.left, this.top];
},
set: function (props) {
Object.assign(this, props);
},
};
}
/**
* 运行单个测试
*/
async function runTest(testName, testFunction) {
testResults.totalTests++;
try {
console.log(`🧪 运行测试: ${testName}`);
await testFunction();
testResults.passedTests++;
console.log(`✅ 测试通过: ${testName}`);
} catch (error) {
testResults.failedTests++;
testResults.errors.push({ testName, error: error.message });
console.error(`❌ 测试失败: ${testName}`, error);
}
}
/**
* 测试CPU管理器基本功能
*/
async function testCPUManagerBasics() {
const manager = new LiquifyCPUManager();
const testImageData = createTestImageData();
// 初始化
manager.initialize(testImageData);
// 设置参数
manager.setParams({
size: 50,
pressure: 0.5,
distortion: 0.3,
power: 0.8,
});
// 设置模式
manager.setMode("push");
// 应用变形
const result = manager.applyDeformation(50, 50);
if (
!result ||
result.width !== testImageData.width ||
result.height !== testImageData.height
) {
throw new Error("CPU管理器变形结果无效");
}
// 测试不同模式
const modes = [
"push",
"clockwise",
"counterclockwise",
"pinch",
"expand",
"reconstruct",
];
for (const mode of modes) {
manager.setMode(mode);
const modeResult = manager.applyDeformation(25, 25);
if (!modeResult) {
throw new Error(`模式 ${mode} 变形失败`);
}
}
}
/**
* 测试WebGL管理器基本功能
*/
async function testWebGLManagerBasics() {
const manager = new LiquifyWebGLManager();
const testImageData = createTestImageData();
try {
// 初始化
manager.initialize(testImageData);
// 检查WebGL是否可用
if (!manager.initialized) {
console.warn("WebGL不可用跳过WebGL测试");
return;
}
// 设置参数
manager.setParams({
size: 50,
pressure: 0.5,
distortion: 0.3,
power: 0.8,
});
// 设置模式
manager.setMode("push");
// 应用变形
const result = manager.applyDeformation(50, 50);
if (
!result ||
result.width !== testImageData.width ||
result.height !== testImageData.height
) {
throw new Error("WebGL管理器变形结果无效");
}
} catch (error) {
if (error.message.includes("WebGL")) {
console.warn("WebGL不支持跳过WebGL测试");
return;
}
throw error;
}
}
/**
* 测试实时更新器功能
*/
async function testRealTimeUpdater() {
const canvas = createTestCanvas();
const testImageData = createTestImageData();
const mockFabricObject = createMockFabricObject(testImageData);
// 模拟fabric Canvas
const fabricCanvas = {
getObjects: () => [mockFabricObject],
remove: () => {},
insertAt: () => {},
renderAll: () => {},
};
const updater = new LiquifyRealTimeUpdater(fabricCanvas);
// 设置目标对象
updater.setTargetObject(mockFabricObject);
// 测试快速更新
await updater.updateImage(testImageData, true);
// 测试完整更新
await updater.updateImage(testImageData, false);
// 测试待处理更新
updater.pendingImageData = testImageData;
await updater.processPendingUpdates();
// 清理
updater.dispose();
}
/**
* 测试增强液化管理器
*/
async function testEnhancedLiquifyManager() {
const manager = new EnhancedLiquifyManager();
const testImageData = createTestImageData();
const mockFabricObject = createMockFabricObject(testImageData);
// 初始化
const result = await manager.prepareForLiquify(mockFabricObject);
if (!result || !result.originalImageData) {
throw new Error("增强液化管理器初始化失败");
}
// 设置参数
manager.setParams({
size: 50,
pressure: 0.5,
distortion: 0.3,
power: 0.8,
});
// 应用液化
const liquifyResult = await manager.applyLiquify(
mockFabricObject,
"push",
{ size: 50, pressure: 0.5, distortion: 0.3, power: 0.8 },
50,
50
);
if (!liquifyResult) {
throw new Error("液化应用失败");
}
}
/**
* 测试坐标转换准确性
*/
async function testCoordinateConversion() {
const testImageData = createTestImageData(200, 200);
const mockFabricObject = createMockFabricObject(testImageData);
// 测试不同的缩放情况
const testCases = [
{ scaleX: 1, scaleY: 1, flipX: false, flipY: false },
{ scaleX: 2, scaleY: 2, flipX: false, flipY: false },
{ scaleX: 0.5, scaleY: 0.5, flipX: false, flipY: false },
{ scaleX: 1, scaleY: 1, flipX: true, flipY: false },
{ scaleX: 1, scaleY: 1, flipX: false, flipY: true },
];
for (const testCase of testCases) {
Object.assign(mockFabricObject, testCase);
// 模拟坐标转换函数(简化版)
const fabricX = 100,
fabricY = 100;
// 创建变换矩阵模拟
const transform = mockFabricObject.calcTransformMatrix();
// 基本的坐标边界检查
const localX = (fabricX - mockFabricObject.left) / mockFabricObject.scaleX;
const localY = (fabricY - mockFabricObject.top) / mockFabricObject.scaleY;
if (
localX < -mockFabricObject.width / 2 ||
localX > mockFabricObject.width / 2 ||
localY < -mockFabricObject.height / 2 ||
localY > mockFabricObject.height / 2
) {
// 坐标在对象外部,这是正常情况
continue;
}
// 转换到图像坐标
let imageX =
(localX + mockFabricObject.width / 2) *
(testImageData.width / mockFabricObject.width);
let imageY =
(localY + mockFabricObject.height / 2) *
(testImageData.height / mockFabricObject.height);
// 处理翻转
if (mockFabricObject.flipX) {
imageX = testImageData.width - imageX;
}
if (mockFabricObject.flipY) {
imageY = testImageData.height - imageY;
}
// 验证结果在合理范围内
if (
imageX < 0 ||
imageX >= testImageData.width ||
imageY < 0 ||
imageY >= testImageData.height
) {
throw new Error(`坐标转换结果超出图像范围: (${imageX}, ${imageY})`);
}
}
}
/**
* 测试性能表现
*/
async function testPerformance() {
const manager = new LiquifyCPUManager();
const testImageData = createTestImageData(400, 400); // 更大的图像
manager.initialize(testImageData);
manager.setParams({
size: 50,
pressure: 0.5,
distortion: 0.3,
power: 0.8,
});
manager.setMode("push");
// 测试多次操作的性能
const startTime = performance.now();
const operationCount = 100;
for (let i = 0; i < operationCount; i++) {
const x = Math.random() * testImageData.width;
const y = Math.random() * testImageData.height;
manager.applyDeformation(x, y);
}
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTime = totalTime / operationCount;
console.log(
`性能测试结果: ${operationCount} 次操作,总耗时 ${totalTime.toFixed(
2
)}ms平均 ${avgTime.toFixed(2)}ms/次`
);
// 验证性能阈值每次操作不应超过50ms
if (avgTime > 50) {
throw new Error(
`性能不达标:平均操作时间 ${avgTime.toFixed(2)}ms 超过阈值 50ms`
);
}
}
/**
* 测试内存管理
*/
async function testMemoryManagement() {
const initialMemory = performance.memory
? performance.memory.usedJSHeapSize
: 0;
// 创建多个管理器实例
const managers = [];
for (let i = 0; i < 10; i++) {
const manager = new LiquifyCPUManager();
const testImageData = createTestImageData(200, 200);
manager.initialize(testImageData);
managers.push(manager);
}
// 销毁所有管理器
for (const manager of managers) {
if (manager.destroy) {
manager.destroy();
}
}
// 强制垃圾回收(如果可用)
if (window.gc) {
window.gc();
}
const finalMemory = performance.memory
? performance.memory.usedJSHeapSize
: 0;
const memoryIncrease = finalMemory - initialMemory;
console.log(
`内存测试: 初始 ${(initialMemory / 1024 / 1024).toFixed(2)}MB, 最终 ${(
finalMemory /
1024 /
1024
).toFixed(2)}MB, 增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
);
// 验证内存增长不超过10MB基本的内存泄漏检查
if (memoryIncrease > 10 * 1024 * 1024) {
console.warn(
`潜在内存泄漏: 内存增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
);
}
}
/**
* 运行所有集成测试
*/
export async function runLiquifyIntegrationTests() {
console.log("🚀 开始液化功能集成测试...");
// 重置测试结果
testResults = {
totalTests: 0,
passedTests: 0,
failedTests: 0,
errors: [],
};
try {
// 基础功能测试
await runTest("CPU管理器基本功能", testCPUManagerBasics);
await runTest("WebGL管理器基本功能", testWebGLManagerBasics);
await runTest("实时更新器功能", testRealTimeUpdater);
await runTest("增强液化管理器", testEnhancedLiquifyManager);
// 高级功能测试
await runTest("坐标转换准确性", testCoordinateConversion);
await runTest("性能表现", testPerformance);
await runTest("内存管理", testMemoryManagement);
} catch (error) {
console.error("集成测试出现严重错误:", error);
}
// 输出测试结果
console.log("\n📊 液化功能集成测试结果:");
console.log(`总测试数: ${testResults.totalTests}`);
console.log(`通过: ${testResults.passedTests}`);
console.log(`失败: ${testResults.failedTests}`);
console.log(
`成功率: ${(
(testResults.passedTests / testResults.totalTests) *
100
).toFixed(1)}%`
);
if (testResults.errors.length > 0) {
console.log("\n❌ 失败的测试:");
testResults.errors.forEach(({ testName, error }) => {
console.log(` - ${testName}: ${error}`);
});
}
return testResults;
}
/**
* 在浏览器控制台中运行测试
*/
if (typeof window !== "undefined") {
window.runLiquifyIntegrationTests = runLiquifyIntegrationTests;
console.log("💡 使用 window.runLiquifyIntegrationTests() 运行液化集成测试");
}

View File

@@ -0,0 +1,222 @@
/**
* 液化功能测试和验证
* 用于测试液化推拉算法和坐标转换修复
*/
// 测试坐标转换函数
export function testCoordinateConversion() {
console.log("开始测试坐标转换...");
// 模拟fabric对象
const mockFabricObject = {
left: 100,
top: 100,
width: 200,
height: 200,
scaleX: 1.5,
scaleY: 1.5,
angle: 0,
flipX: false,
flipY: false,
calcTransformMatrix: function () {
// 简化的变换矩阵计算
return [this.scaleX, 0, 0, this.scaleY, this.left, this.top];
},
};
// 模拟图像数据
const mockImageData = {
width: 400,
height: 400,
};
// 测试不同的画布坐标
const testCoordinates = [
{ x: 150, y: 150 }, // 左上角
{ x: 200, y: 200 }, // 中心
{ x: 250, y: 250 }, // 右下角
];
testCoordinates.forEach((coord, index) => {
console.log(`测试坐标 ${index + 1}: (${coord.x}, ${coord.y})`);
// 这里应该调用实际的坐标转换函数
// const imageCoords = _convertFabricCoordsToImageCoords(coord.x, coord.y);
// 模拟转换结果
const expectedImageX =
((coord.x - mockFabricObject.left) / mockFabricObject.scaleX +
mockFabricObject.width / 2) *
(mockImageData.width / mockFabricObject.width);
const expectedImageY =
((coord.y - mockFabricObject.top) / mockFabricObject.scaleY +
mockFabricObject.height / 2) *
(mockImageData.height / mockFabricObject.height);
console.log(
` 预期图像坐标: (${expectedImageX.toFixed(2)}, ${expectedImageY.toFixed(
2
)})`
);
});
console.log("坐标转换测试完成");
}
// 测试推拉算法性能
export function testPushAlgorithmPerformance() {
console.log("开始测试推拉算法性能...");
const startTime = performance.now();
// 模拟连续的推拉操作
const operations = 100;
const movements = [];
for (let i = 0; i < operations; i++) {
// 模拟鼠标移动
const x = 200 + Math.sin(i * 0.1) * 50;
const y = 200 + Math.cos(i * 0.1) * 50;
movements.push({ x, y, timestamp: Date.now() });
// 模拟液化计算
const movementLength =
i > 0
? Math.sqrt(
Math.pow(x - movements[i - 1].x, 2) +
Math.pow(y - movements[i - 1].y, 2)
)
: 0;
if (movementLength > 0.5) {
// 模拟变形计算
const pressure = 0.8;
const power = 0.8;
const velocityFactor = Math.min(movementLength * 0.1, 1.0);
const pushStrength = pressure * power * velocityFactor * 0.5;
// 记录计算结果
movements[i].strength = pushStrength;
}
}
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTimePerOperation = totalTime / operations;
console.log(`推拉算法性能测试结果:`);
console.log(` 总操作数: ${operations}`);
console.log(` 总耗时: ${totalTime.toFixed(2)}ms`);
console.log(` 平均每次操作耗时: ${avgTimePerOperation.toFixed(2)}ms`);
console.log(` 预估FPS: ${(1000 / avgTimePerOperation).toFixed(1)}fps`);
return {
totalTime,
avgTimePerOperation,
estimatedFps: 1000 / avgTimePerOperation,
};
}
// 测试缩放一致性
export function testScaleConsistency() {
console.log("开始测试缩放一致性...");
// 模拟不同缩放比例的对象
const scaleFactors = [0.5, 1.0, 1.5, 2.0, 2.5];
scaleFactors.forEach((scale) => {
console.log(`测试缩放比例: ${scale}`);
// 模拟fabric对象
const fabricObject = {
width: 200,
height: 200,
scaleX: scale,
scaleY: scale,
left: 300,
top: 300,
};
// 模拟图像数据(原始尺寸)
const imageData = {
width: 400,
height: 400,
};
// 测试坐标转换
const canvasCoord = { x: 350, y: 350 }; // 画布坐标
// 计算预期的图像坐标(修复后的逻辑)
const localX = (canvasCoord.x - fabricObject.left) / fabricObject.scaleX;
const localY = (canvasCoord.y - fabricObject.top) / fabricObject.scaleY;
const imageX =
(localX + fabricObject.width / 2) *
(imageData.width / fabricObject.width);
const imageY =
(localY + fabricObject.height / 2) *
(imageData.height / fabricObject.height);
console.log(` 画布坐标: (${canvasCoord.x}, ${canvasCoord.y})`);
console.log(` 本地坐标: (${localX.toFixed(2)}, ${localY.toFixed(2)})`);
console.log(` 图像坐标: (${imageX.toFixed(2)}, ${imageY.toFixed(2)})`);
// 验证图像坐标是否在合理范围内
const isValid =
imageX >= 0 &&
imageX < imageData.width &&
imageY >= 0 &&
imageY < imageData.height;
console.log(` 坐标有效性: ${isValid ? "✓" : "✗"}`);
});
console.log("缩放一致性测试完成");
}
// 运行所有测试
export function runAllTests() {
console.log("=== 液化功能测试开始 ===");
try {
testCoordinateConversion();
console.log("");
const perfResult = testPushAlgorithmPerformance();
console.log("");
testScaleConsistency();
console.log("");
console.log("=== 液化功能测试完成 ===");
console.log(
`推荐配置: 节流时间 ${Math.max(
16,
perfResult.avgTimePerOperation * 2
).toFixed(0)}ms`
);
return {
coordinateConversion: "通过",
performance: perfResult,
scaleConsistency: "通过",
};
} catch (error) {
console.error("测试过程中出现错误:", error);
return {
error: error.message,
};
}
}
// 在浏览器控制台中运行测试
if (typeof window !== "undefined") {
window.liquifyTests = {
testCoordinateConversion,
testPushAlgorithmPerformance,
testScaleConsistency,
runAllTests,
};
console.log("液化测试工具已加载,可通过 window.liquifyTests 访问");
}

View File

@@ -0,0 +1,820 @@
import { ReorderChildLayersCommand } from "../commands/LayerCommands";
import { optimizeCanvasRendering } from "./helper";
import { findLayerRecursively, LayerType } from "./layerHelper";
/**
* 图层排序工具类
* 提供图层排序、重新排列画布对象等功能
* 基于fabric.js 5.3.1版本开发
*/
export class LayerSort {
/**
* 构造函数
* @param {Object} canvas fabric.js画布实例
* @param {Object} layers 图层数组响应式引用
*/
constructor(canvas, layers, options = {}) {
this.canvas = canvas;
this.layers = layers;
this.commandManager = options.commandManager || null; // 命令管理器(可选)
}
/**
* 重新排列画布上的对象以匹配图层顺序
* 使用 fabric.js 的 moveTo 方法直接调整对象层级,无需清空画布
*/
async rearrangeObjects() {
if (!this.canvas) return;
const canvasObjects = this.canvas.getObjects();
if (canvasObjects.length === 0) return;
// 使用画布渲染优化
await optimizeCanvasRendering(this.canvas, () => {
// 计算每个对象应该在的 z-index 位置
const objectZIndexMap = this.calculateObjectZIndexes();
// 按照新的 z-index 排序对象
const sortedObjects = canvasObjects
.map((obj) => ({
object: obj,
targetZIndex: objectZIndexMap.get(obj.id) ?? -1,
}))
.filter((item) => item.targetZIndex >= 0) // 过滤掉无效对象
.sort((a, b) => a.targetZIndex - b.targetZIndex);
// 使用 fabric.js 的 moveTo 方法重新排序
sortedObjects.forEach((item, index) => {
const currentIndex = this.canvas.getObjects().indexOf(item.object);
if (currentIndex !== index && currentIndex !== -1) {
// 将对象移动到正确的位置
this.canvas.moveTo(item.object, index);
}
});
});
}
/**
* 计算每个对象在画布中应该的 z-index
* 考虑图层类型、组结构和子图层
* @returns {Map} 对象ID到z-index的映射
*/
calculateObjectZIndexes() {
const zIndexMap = new Map();
let currentZIndex = 0;
// 按照图层在数组中的顺序从后往前遍历(数组末尾 = 画布底层)
for (let i = this.layers.value.length - 1; i >= 0; i--) {
const layer = this.layers.value[i];
// 跳过不可见图层
if (!layer.visible) {
continue;
}
// 处理不同类型的图层
if (layer.isBackground && layer.fabricObject) {
// 背景图层对象放在最底层
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (layer.isFixed && layer.fabricObjects) {
// 固定图层对象
layer.fabricObjects.forEach((obj) => {
if (obj?.id) {
zIndexMap.set(obj.id, currentZIndex++);
}
});
} else if (!layer.isBackground && !layer.isFixed) {
// 普通图层
currentZIndex = this.processLayerObjects(
layer,
currentZIndex,
zIndexMap
);
}
}
return zIndexMap;
}
/**
* 处理图层对象,包括组和子图层的情况
* @param {Object} layer 图层对象
* @param {number} currentZIndex 当前z-index
* @param {Map} zIndexMap z-index映射表
* @returns {number} 更新后的z-index
*/
processLayerObjects(layer, currentZIndex, zIndexMap) {
// 检查是否有子图层(组图层)
if (layer.children?.length > 0) {
// 处理每个子图层
// 按照图层在数组中的顺序从后往前遍历(数组末尾 = 画布底层)
for (let i = layer.children.length - 1; i >= 0; i--) {
const childLayer = layer.children[i];
// 跳过不可见图层
if (!childLayer.visible) {
continue;
}
for (let j = childLayer.fabricObjects.length - 1; j >= 0; j--) {
const obj = childLayer.fabricObjects[j];
if (obj?.id) {
zIndexMap.set(obj.id, currentZIndex++);
}
}
}
}
// 处理图层本身的对象
if (Array.isArray(layer.fabricObjects)) {
for (let j = layer.fabricObjects.length - 1; j >= 0; j--) {
const obj = layer.fabricObjects[j];
if (obj?.id) {
zIndexMap.set(obj.id, currentZIndex++);
}
}
}
return currentZIndex;
}
/**
* 获取指定图层的子图层,按照正确顺序排列
* @param {string} parentLayerId 父图层ID
* @returns {Array} 子图层数组
*/
getChildLayersInOrder(parentLayerId) {
// 获取所有子图层
const childLayers =
this.layers.value.filter((layer) => layer.id === parentLayerId)
?.children || [];
return childLayers;
}
/**
* 根据ID获取图层
* @param {string} layerId 图层ID
* @returns {Object|null} 图层对象或null
*/
getLayerById(layerId) {
if (!layerId || !this.layers.value) return null;
return this.layers.value.find((layer) => layer.id === layerId) ?? null;
}
/**
* 批量重新排列对象(异步版本,适用于大量对象)
* @returns {Promise<void>}
*/
async rearrangeObjectsAsync() {
if (!this.canvas) return;
const canvasObjects = this.canvas.getObjects();
if (canvasObjects.length === 0) return;
return new Promise((resolve) => {
// 使用 requestAnimationFrame 进行异步处理
requestAnimationFrame(() => {
this.canvas.renderOnAddRemove = false;
try {
const objectZIndexMap = this.calculateObjectZIndexes();
const sortedObjects = canvasObjects
.map((obj) => ({
object: obj,
targetZIndex: objectZIndexMap.get(obj.id) ?? -1,
}))
.filter((item) => item.targetZIndex >= 0)
.sort((a, b) => a.targetZIndex - b.targetZIndex);
// 分批处理,避免一次性处理太多对象
const batchSize = LayerSortConstants.BATCH_SIZE;
let currentBatch = 0;
const processBatch = () => {
const start = currentBatch * batchSize;
const end = Math.min(start + batchSize, sortedObjects.length);
for (let i = start; i < end; i++) {
const item = sortedObjects[i];
const currentIndex = this.canvas
.getObjects()
.indexOf(item.object);
if (currentIndex !== i && currentIndex !== -1) {
this.canvas.moveTo(item.object, i);
}
}
currentBatch++;
if (end < sortedObjects.length) {
// 继续处理下一批
requestAnimationFrame(processBatch);
} else {
// 所有批次处理完成
this.canvas.renderOnAddRemove = true;
this.canvas.renderAll();
resolve();
}
};
processBatch();
} catch (error) {
this.canvas.renderOnAddRemove = true;
this.canvas.renderAll();
console.error("重新排列对象时出错:", error);
resolve();
}
});
});
}
/**
* 验证对象顺序是否正确
* @returns {boolean} 顺序是否正确
*/
validateObjectOrder() {
const canvasObjects = this.canvas.getObjects();
const objectZIndexMap = this.calculateObjectZIndexes();
for (let i = 0; i < canvasObjects.length - 1; i++) {
const currentObj = canvasObjects[i];
const nextObj = canvasObjects[i + 1];
const currentZIndex = objectZIndexMap.get(currentObj.id);
const nextZIndex = objectZIndexMap.get(nextObj.id);
if (currentZIndex !== undefined && nextZIndex !== undefined) {
if (currentZIndex > nextZIndex) {
return false; // 顺序不正确
}
}
}
return true; // 顺序正确
}
/**
* 图层排序规则:背景图层 > 固定图层 > 普通图层
* @param {Array} layers 图层数组
* @returns {Array} 排序后的图层数组
*/
sortLayers(layers = null) {
const targetLayers = layers ?? this.layers.value;
return [...targetLayers].sort((a, b) => {
// 如果a是背景图层它应该排在后面最底层
if (a.isBackground) return 1;
// 如果b是背景图层它应该排在后面最底层
if (b.isBackground) return -1;
// 如果a是固定图层而b不是固定图层a应该排在后面固定图层在普通图层下方
if (a.isFixed && !b.isFixed) return 1;
// 如果b是固定图层而a不是固定图层b应该排在后面固定图层在普通图层下方
if (b.isFixed && !a.isFixed) return -1;
// 其他情况保持原有顺序
return 0;
});
}
/**
* 获取图层在排序后的正确插入位置
* @param {Object} newLayer 新图层
* @param {string} targetLayerId 目标图层ID插入到该图层之前
* @returns {number} 插入位置索引
*/
getInsertIndex(newLayer, targetLayerId = null) {
if (!targetLayerId) {
// 如果没有指定目标图层,根据图层类型决定插入位置
if (newLayer.isBackground) {
return this.layers.value.length; // 背景图层插入到最后
} else if (newLayer.isFixed) {
// 固定图层插入到背景图层之前
const bgIndex = this.layers.value.findIndex(
(layer) => layer.isBackground
);
return bgIndex !== -1 ? bgIndex : this.layers.value.length;
} else {
// 普通图层插入到固定图层之前
const fixedIndex = this.layers.value.findIndex(
(layer) => layer.isFixed
);
return fixedIndex !== -1 ? fixedIndex : this.layers.value.length;
}
}
// 如果指定了目标图层,插入到目标图层之前
const targetIndex = this.layers.value.findIndex(
(layer) => layer.id === targetLayerId
);
return targetIndex !== -1 ? targetIndex : this.layers.value.length;
}
/**
* 移动图层到指定位置
* @param {string} layerId 要移动的图层ID
* @param {number} newIndex 新位置索引
* @returns {boolean} 是否移动成功
*/
async moveLayerToIndex({ parentId, oldIndex, newIndex, layerId }) {
// 检查父图层是否存在
// const parentLayer = this.getLayerById(parentId);
const { layer: childLayer, parent: parentLayer } = findLayerRecursively(
this.layers.value,
layerId,
parentId
);
if (!parentLayer) {
console.warn(`父图层 ${parentId} 不存在`);
return false;
}
// 获取所有子图层
const childLayers = parentLayer?.children || [];
// 检查索引有效性
if (
oldIndex < 0 ||
newIndex < 0 ||
oldIndex >= childLayers.length ||
newIndex >= childLayers.length
) {
console.warn("子图层排序索引无效");
return false;
}
// 检查是否是同一位置
if (oldIndex === newIndex) {
return true;
}
// 验证图层ID
const layer = childLayers[oldIndex];
if (!layer || layer.id !== layerId) {
console.warn("子图层ID与索引不匹配");
return false;
}
// 更新父图层的children数组 - 执行命令
const command = new ReorderChildLayersCommand({
parentId,
oldIndex,
newIndex,
layerId,
layers: this.layers,
canvas: this.canvas,
layerSort: this, // 传入当前实例
});
if (this.commandManager) {
await this.commandManager?.execute(command);
} else {
await command?.execute?.();
}
return true;
}
/**
* 获取图层的有效移动范围
* @param {string} layerId 图层ID
* @returns {Object} 包含最小和最大索引的对象
*/
getLayerMoveRange(layerId) {
const layer = this.getLayerById(layerId);
if (!layer || layer.isBackground || layer.isFixed) {
return { minIndex: -1, maxIndex: -1 };
}
// 普通图层只能在普通图层范围内移动
const normalLayers = this.layers.value
.map((layer, index) => ({ layer, index }))
.filter((item) => !item.layer.isBackground && !item.layer.isFixed);
if (normalLayers.length === 0) {
return { minIndex: -1, maxIndex: -1 };
}
return {
minIndex: normalLayers[0].index,
maxIndex: normalLayers[normalLayers.length - 1].index,
};
}
/**
* 拖拽排序图层
* @param {number} oldIndex 原索引
* @param {number} newIndex 新索引
* @param {string} layerId 图层ID
* @returns {boolean} 是否排序成功
*/
async reorderLayers(oldIndex, newIndex, layerId) {
// 检查索引有效性
if (
oldIndex < 0 ||
newIndex < 0 ||
oldIndex >= this.layers.value.length ||
newIndex >= this.layers.value.length
) {
console.warn("图层排序索引无效");
return false;
}
// 检查是否是同一位置
if (oldIndex === newIndex) {
return true;
}
// 获取要移动的图层
const layer = this.layers.value[oldIndex];
if (!layer || layer.id !== layerId) {
console.warn("图层ID与索引不匹配");
return false;
}
// 检查是否是背景层或固定层(不允许排序)
if (layer.isBackground || layer.isFixed) {
console.warn("背景层和固定层不能参与排序");
return false;
}
// 检查目标位置是否合法(不能移到背景层或固定层的位置)
const targetLayer = this.layers.value[newIndex];
if (targetLayer && (targetLayer.isBackground || targetLayer.isFixed)) {
console.warn("不能移动到背景层或固定层的位置");
return false;
}
// 执行排序
const layersArray = [...this.layers.value];
const [movedLayer] = layersArray.splice(oldIndex, 1);
layersArray.splice(newIndex, 0, movedLayer);
this.layers.value = layersArray;
// 重新排列画布对象
await this.rearrangeObjects();
return true;
}
/**
* 子图层排序
* @param {string} parentId 父图层ID
* @param {number} oldIndex 原索引
* @param {number} newIndex 新索引
* @param {string} layerId 子图层ID
* @returns {boolean} 是否排序成功
*/
async reorderChildLayers(parentId, oldIndex, newIndex, layerId) {
// 检查父图层是否存在
// const parentLayer = this.getLayerById(parentId);
const { layer: childLayer, parent: parentLayer } = findLayerRecursively(
this.layers.value,
layerId,
parentId
);
if (!parentLayer) {
console.warn(`父图层 ${parentId} 不存在`);
return false;
}
// 获取所有子图层
const childLayers = parentLayer?.children || [];
// 检查索引有效性
if (
oldIndex < 0 ||
newIndex < 0 ||
oldIndex >= childLayers.length ||
newIndex >= childLayers.length
) {
console.warn("子图层排序索引无效");
return false;
}
// 检查是否是同一位置
if (oldIndex === newIndex) {
return true;
}
// 验证图层ID
const layer = childLayers[oldIndex];
if (!layer || layer.id !== layerId) {
console.warn("子图层ID与索引不匹配");
return false;
}
// 更新父图层的children数组 - 执行命令
const command = ReorderChildLayersCommand({
parentId,
oldIndex,
newIndex,
layerId,
layers: this.layers,
canvas: this.canvas,
layerSort: this, // 传入当前实例
});
if (this.commandManager) {
await this.commandManager?.execute(command);
} else {
await command?.execute?.();
}
return true;
}
/**
* 智能排序 - 根据对象类型和位置自动调整图层顺序
* @param {Array} targetLayerIds 要排序的图层ID数组
* @returns {boolean} 是否排序成功
*/
async smartSort(targetLayerIds = null) {
const layersToSort = targetLayerIds
? this.layers.value.filter((layer) => targetLayerIds.includes(layer.id))
: this.layers.value.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
if (layersToSort.length <= 1) return true;
// 按照对象类型和位置进行智能排序
layersToSort.sort((a, b) => {
const aWeight = this.getLayerSortWeight(a);
const bWeight = this.getLayerSortWeight(b);
if (aWeight !== bWeight) {
return bWeight - aWeight; // 权重高的在上层
}
// 权重相同时按照Y坐标排序Y值小的在上层
const aY = this.getLayerAverageY(a);
const bY = this.getLayerAverageY(b);
return aY - bY;
});
// 更新图层顺序
const sortedLayerIds = layersToSort.map((layer) => layer.id);
const otherLayers = this.layers.value.filter(
(layer) => !sortedLayerIds.includes(layer.id)
);
// 重新组织图层数组:保持背景层和固定层的位置
const newLayers = [];
// 添加普通图层(已排序)
newLayers.push(...layersToSort);
// 添加其他普通图层
otherLayers.forEach((layer) => {
if (!layer.isBackground && !layer.isFixed) {
newLayers.push(layer);
}
});
// 添加固定图层
otherLayers.forEach((layer) => {
if (layer.isFixed) {
newLayers.push(layer);
}
});
// 添加背景图层
otherLayers.forEach((layer) => {
if (layer.isBackground) {
newLayers.push(layer);
}
});
this.layers.value = newLayers;
await this.rearrangeObjects();
return true;
}
/**
* 获取图层的排序权重
* @param {Object} layer 图层对象
* @returns {number} 排序权重
*/
getLayerSortWeight(layer) {
const weightMap = LayerSortConstants.LAYER_PRIORITY;
if (layer.isBackground) return weightMap[LayerType.BACKGROUND];
if (layer.isFixed) return weightMap[LayerType.FIXED];
if (layer.children?.length > 0) return weightMap[LayerType.GROUP];
// 根据对象类型调整权重
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
const firstObj = layer.fabricObjects[0];
if (firstObj.type === "text") return weightMap[LayerType.NORMAL] + 10;
if (firstObj.type === "image") return weightMap[LayerType.NORMAL] + 5;
}
return weightMap[LayerType.NORMAL];
}
/**
* 获取图层对象的平均Y坐标
* @param {Object} layer 图层对象
* @returns {number} 平均Y坐标
*/
getLayerAverageY(layer) {
let totalY = 0;
let count = 0;
if (layer.fabricObject) {
totalY += layer.fabricObject.top || 0;
count++;
}
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
layer.fabricObjects.forEach((obj) => {
totalY += obj.top || 0;
count++;
});
}
return count > 0 ? totalY / count : 0;
}
/**
* 优化图层结构 - 清理空图层、合并相邻相似图层等
* @returns {Object} 优化结果统计
*/
async optimizeLayerStructure() {
const stats = {
removedEmptyLayers: 0,
mergedLayers: 0,
reorderedLayers: 0,
};
// 清理空图层
const emptyLayers = this.layers.value.filter(
(layer) =>
!layer.isBackground &&
!layer.isFixed &&
(!layer.fabricObjects || layer.fabricObjects.length === 0) &&
(!layer.children || layer.children.length === 0)
);
emptyLayers.forEach((layer) => {
const index = this.layers.value.findIndex((l) => l.id === layer.id);
if (index !== -1) {
this.layers.value.splice(index, 1);
stats.removedEmptyLayers++;
}
});
// 重新排序以确保正确的层级关系
const wasReordered = this.sortLayers();
if (wasReordered) {
stats.reorderedLayers = this.layers.value.length;
await this.rearrangeObjects();
}
return stats;
}
}
/**
* 创建图层排序工具实例
* @param {Object} canvas fabric.js画布实例
* @param {Object} layers 图层数组响应式引用
* @returns {LayerSort} 图层排序工具实例
*/
export const createLayerSort = (canvas, layers, options = {}) => {
return new LayerSort(canvas, layers, options);
};
/**
* 图层排序相关的常量
*/
export const LayerSortConstants = {
// 图层类型优先级(数值越大优先级越高,在画布中越靠上)
LAYER_PRIORITY: {
[LayerType.BACKGROUND]: 0,
[LayerType.FIXED]: 1,
[LayerType.NORMAL]: 2,
[LayerType.GROUP]: 2,
},
// 批处理大小
BATCH_SIZE: 50,
// 性能阈值
PERFORMANCE_THRESHOLD: {
OBJECTS_COUNT: 100, // 超过此数量使用异步处理
LAYERS_COUNT: 20, // 超过此数量使用分批处理
},
};
/**
* 图层排序辅助函数
*/
export const LayerSortUtils = {
/**
* 检查是否需要异步处理
* @param {number} objectsCount 对象数量
* @param {number} layersCount 图层数量
* @returns {boolean} 是否需要异步处理
*/
shouldUseAsyncProcessing(objectsCount, layersCount) {
return (
objectsCount > LayerSortConstants.PERFORMANCE_THRESHOLD.OBJECTS_COUNT ||
layersCount > LayerSortConstants.PERFORMANCE_THRESHOLD.LAYERS_COUNT
);
},
/**
* 获取图层的排序权重
* @param {Object} layer 图层对象
* @returns {number} 排序权重
*/
getLayerSortWeight(layer) {
if (layer.isBackground)
return LayerSortConstants.LAYER_PRIORITY[LayerType.BACKGROUND];
if (layer.isFixed)
return LayerSortConstants.LAYER_PRIORITY[LayerType.FIXED];
if (layer.children?.length > 0)
return LayerSortConstants.LAYER_PRIORITY[LayerType.GROUP];
return LayerSortConstants.LAYER_PRIORITY[LayerType.NORMAL];
},
/**
* 比较两个图层的排序优先级
* @param {Object} layerA 图层A
* @param {Object} layerB 图层B
* @returns {number} 比较结果
*/
compareLayerPriority(layerA, layerB) {
const weightA = this.getLayerSortWeight(layerA);
const weightB = this.getLayerSortWeight(layerB);
return weightB - weightA; // 权重高的排在前面(画布上层)
},
};
/**
* 图层排序混入方法 - 用于在 LayerManager 中使用
*/
export const LayerSortMixin = {
/**
* 初始化图层排序工具
*/
initLayerSort() {
this.layerSort = new LayerSort(this.canvas, this.layers);
},
/**
* 重新排列画布对象
*/
rearrangeCanvasObjects() {
if (this.layerSort) {
// 检查是否需要异步处理
const objectsCount = this.canvas?.getObjects()?.length || 0;
const layersCount = this.layers?.value?.length || 0;
if (LayerSortUtils.shouldUseAsyncProcessing(objectsCount, layersCount)) {
return this.layerSort.rearrangeObjectsAsync();
} else {
this.layerSort.rearrangeObjects();
return Promise.resolve();
}
}
},
/**
* 排序图层
*/
sortLayersWithTool() {
if (this.layerSort) {
this.layers.value = this.layerSort.sortLayers();
return this.layerSort.rearrangeObjects();
}
},
/**
* 智能排序图层
*/
smartSortLayers(targetLayerIds = null) {
if (this.layerSort) {
return this.layerSort.smartSort(targetLayerIds);
}
return false;
},
/**
* 优化图层结构
*/
optimizeLayerStructure() {
if (this.layerSort) {
return this.layerSort.optimizeLayerStructure();
}
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
},
};

View File

@@ -1,4 +1,4 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import { canvasConfig } from "../config/canvasConfig";
/**

View File

@@ -434,25 +434,324 @@ export function createCancellablePromise(executor) {
};
}
// 导出所有工具函数
export default {
deepCompare,
deepClone,
applyDiff,
throttle,
debounce,
generateId,
formatFileSize,
formatDuration,
isValidCommand,
isPromise,
safeJSONParse,
safeJSONStringify,
getObjectDepth,
getObjectSize,
checkBrowserSupport,
delay,
retry,
batchProcess,
createCancellablePromise,
/**
* 增强版检查对象是否在画布中(包括组内对象)
* @param {fabric.Canvas} canvas 画布实例
* @param {fabric.Object} targetObj 目标对象
* @returns {Object} { flag: boolean, object: fabric.Object, parent: fabric.Group|null }
*/
export function objectIsInCanvas(canvas, targetObj) {
if (!canvas || !targetObj) {
return { flag: false, object: null, parent: null };
}
const targetId = targetObj.id;
if (!targetId) {
return { flag: false, object: null, parent: null };
}
// 首先检查顶层对象
const topLevelObjects = canvas.getObjects();
// 直接在顶层查找
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
if (directMatch) {
return { flag: true, object: directMatch, parent: null };
}
// 递归检查组内对象
for (const obj of topLevelObjects) {
if (obj.type === "group") {
const result = findObjectInGroup(obj, targetId);
if (result.found) {
return { flag: true, object: result.object, parent: obj };
}
}
}
return { flag: false, object: null, parent: null };
}
/**
* 在组中递归查找对象
* @param {fabric.Group} group 组对象
* @param {string} targetId 目标对象ID
* @returns {Object} { found: boolean, object: fabric.Object|null }
*/
function findObjectInGroup(group, targetId) {
if (!group || group.type !== "group" || !group.getObjects) {
return { found: false, object: null };
}
const groupObjects = group.getObjects();
for (const obj of groupObjects) {
if (obj.id === targetId) {
return { found: true, object: obj };
}
// 递归检查嵌套组
if (obj.type === "group") {
const nestedResult = findObjectInGroup(obj, targetId);
if (nestedResult.found) {
return nestedResult;
}
}
}
return { found: false, object: null };
}
/**
* 通过ID查找对象增强版
* @param {fabric.Canvas} canvas 画布实例
* @param {string} objectId 对象ID
* @returns {Object} { object: fabric.Object|null, parent: fabric.Group|null }
*/
export function findObjectById(canvas, objectId) {
if (!canvas || !objectId) {
return { object: null, parent: null };
}
const result = objectIsInCanvas(canvas, { id: objectId });
return { object: result.object, parent: result.parent };
}
/**
* 安全移除画布对象(包括组内对象)
* @param {fabric.Canvas} canvas 画布实例
* @param {fabric.Object} targetObj 目标对象
* @returns {boolean} 是否成功移除
*/
export function removeCanvasObjectByObject(canvas, targetObj) {
if (!canvas || !targetObj) {
return false;
}
const result = objectIsInCanvas(canvas, targetObj);
if (!result.flag) {
return false;
}
try {
if (result.parent) {
// 对象在组中,从组中移除
result.parent.removeWithUpdate(result.object);
} else {
// 对象在顶层,直接从画布移除
canvas.remove(result.object);
}
return true;
} catch (error) {
console.error("移除对象时发生错误:", error);
return false;
}
}
/**
* 优化画布渲染 统一渲染完成后才执行
* @param {fabric.Canvas} canvas 画布实例
* @param {Function} callback 渲染执行函数
*/
export const optimizeCanvasRendering = async (canvas, callback) => {
return new Promise((resolve) => {
if (!canvas || typeof callback !== "function") {
resolve();
return;
}
// 暂停渲染以提高性能
canvas.skipTargetFind = true;
// 开始渲染
// 暂停实时渲染和对象查找
const wasRenderOnAddRemove = canvas.renderOnAddRemove;
canvas.renderOnAddRemove = false; // 禁用自动渲染
// 等待下一帧渲染完成
requestAnimationFrame(async () => {
// 恢复渲染设置
canvas.skipTargetFind = false;
canvas.renderOnAddRemove = wasRenderOnAddRemove;
if (isPromise(callback)) {
await callback?.();
} else {
callback?.();
}
canvas.renderAll(); // 确保画布重新渲染 - 同步渲染
resolve();
});
});
};
/**
* 获取对象在画布中的z-index位置
* @param {fabric.Canvas} canvas 画布实例
* @param {fabric.Object} targetObj 目标对象
* @returns {number} z-index位置-1表示未找到
*/
export function getObjectZIndex(canvas, targetObj) {
if (!canvas || !targetObj) {
return -1;
}
const result = objectIsInCanvas(canvas, targetObj);
if (!result.flag) {
return -1;
}
if (result.parent) {
// 对象在组中,返回组在画布中的位置
const allObjects = canvas.getObjects();
return allObjects.indexOf(result.parent);
} else {
// 对象在顶层,直接返回在画布中的位置
const allObjects = canvas.getObjects();
return allObjects.indexOf(result.object);
}
}
/**
* 在指定的z-index位置插入对象
* @param {fabric.Canvas} canvas 画布实例
* @param {fabric.Object} object 要插入的对象
* @param {number} zIndex z-index位置
* @param {boolean} renderAll 是否立即渲染默认true
* @returns {boolean} 是否成功插入
*/
export function insertObjectAtZIndex(canvas, object, zIndex, renderAll = true) {
if (!canvas || !object || zIndex < 0) {
return false;
}
try {
// 确保z-index不超过当前对象数量
const maxIndex = canvas.getObjects().length;
const safeZIndex = Math.min(zIndex, maxIndex);
canvas.insertAt(object, safeZIndex, false);
if (renderAll) {
canvas.renderAll();
}
return true;
} catch (error) {
console.error("插入对象到指定z-index位置失败:", error);
return false;
}
}
/**
* 移动对象到指定的z-index位置
* @param {fabric.Canvas} canvas 画布实例
* @param {fabric.Object} object 要移动的对象
* @param {number} zIndex 目标z-index位置
* @param {boolean} renderAll 是否立即渲染默认true
* @returns {boolean} 是否成功移动
*/
export function moveObjectToZIndex(canvas, object, zIndex, renderAll = true) {
if (!canvas || !object || zIndex < 0) {
return false;
}
const result = objectIsInCanvas(canvas, object);
if (!result.flag) {
console.warn("对象不在画布中,无法移动");
return false;
}
try {
// 确保z-index不超过当前对象数量-1因为当前对象也在其中
const maxIndex = canvas.getObjects().length - 1;
const safeZIndex = Math.min(zIndex, maxIndex);
result.object.moveTo(safeZIndex);
if (renderAll) {
canvas.renderAll();
}
return true;
} catch (error) {
console.error("移动对象到指定z-index位置失败:", error);
return false;
}
}
/**
* 交换两个对象的z-index位置
* @param {fabric.Canvas} canvas 画布实例
* @param {fabric.Object} obj1 第一个对象
* @param {fabric.Object} obj2 第二个对象
* @param {boolean} renderAll 是否立即渲染默认true
* @returns {boolean} 是否成功交换
*/
export function swapObjectsZIndex(canvas, obj1, obj2, renderAll = true) {
if (!canvas || !obj1 || !obj2) {
return false;
}
const zIndex1 = getObjectZIndex(canvas, obj1);
const zIndex2 = getObjectZIndex(canvas, obj2);
if (zIndex1 === -1 || zIndex2 === -1) {
console.warn("其中一个或两个对象不在画布中");
return false;
}
try {
// 先移动z-index较大的对象避免索引冲突
if (zIndex1 > zIndex2) {
moveObjectToZIndex(canvas, obj2, zIndex1, false);
moveObjectToZIndex(canvas, obj1, zIndex2, false);
} else {
moveObjectToZIndex(canvas, obj1, zIndex2, false);
moveObjectToZIndex(canvas, obj2, zIndex1, false);
}
if (renderAll) {
canvas.renderAll();
}
return true;
} catch (error) {
console.error("交换对象z-index位置失败:", error);
return false;
}
}
/**
* 获取画布中所有对象的z-index信息
* @param {fabric.Canvas} canvas 画布实例
* @returns {Array} 包含对象ID和z-index的数组
*/
export function getAllObjectsZIndex(canvas) {
if (!canvas) {
return [];
}
const objects = canvas.getObjects();
return objects.map((obj, index) => ({
id: obj.id || `unnamed_${index}`,
zIndex: index,
type: obj.type,
layerId: obj.layerId,
object: obj,
}));
}
/**
* 按图层ID获取对象的z-index信息
* @param {fabric.Canvas} canvas 画布实例
* @param {string} layerId 图层ID
* @returns {Array} 属于指定图层的对象z-index信息
*/
export function getLayerObjectsZIndex(canvas, layerId) {
if (!canvas || !layerId) {
return [];
}
const allInfo = getAllObjectsZIndex(canvas);
return allInfo.filter((info) => info.layerId === layerId);
}

View File

@@ -1,4 +1,4 @@
//import { fabric } from "fabric-with-all";
import { fabric } from "fabric-with-all";
import { LayerType, OperationType, createBitmapLayer } from "./layerHelper";
// 导入新的复合命令
import { CreateImageLayerCommand } from "../commands/LayerCommands";
@@ -7,6 +7,7 @@ import {
ChangeFixedImageCommand,
AddImageToLayerCommand,
} from "../commands/LayerCommands";
import { generateId } from "./helper";
/**
* 加载并处理图片
@@ -44,6 +45,7 @@ export function loadImage(imageSource, options = {}) {
// 设置图片位置 - 默认居中
if (options.centerOnCanvas !== false) {
fabricImage.set({
id: generateId("fabricImage"),
left: (options.canvasWidth || 800) / 2,
top: (options.canvasHeight || 600) / 2,
originX: "center",

View File

@@ -13,6 +13,7 @@ export const LayerType = {
VIDEO: "video", // 视频图层 (预留)
AUDIO: "audio", // 音频图层 (预留)
FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
};
/**
@@ -461,3 +462,161 @@ export function cloneLayer(layer) {
return clonedLayer;
}
/**
* 递归查找图层(包括子图层)
* @param {Array} layers 图层数组
* @param {string} layerId 要查找的图层ID
* @param {Object} parent 父图层(可选,用于内部递归)
* @returns {Object|null} 包含layer和parent的对象如果未找到返回null
*/
export function findLayerRecursively(layers, layerId, parent = null) {
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;
}
}
}
return null;
}
/**
* 在子图层中递归查找
* @param {Array} children 子图层数组
* @param {string} layerId 要查找的图层ID
* @param {Object} parent 父图层
* @returns {Object|null} 包含layer和parent的对象如果未找到返回null
*/
export function findInChildLayers(children, layerId, parent) {
if (!children || !Array.isArray(children) || !layerId) {
return null;
}
for (const child of children) {
if (child && child.id === layerId) {
return { layer: child, parent };
}
// 如果子图层也是组,继续递归查找
if (
child &&
(child.type === "group" || child.type === LayerType.GROUP) &&
child.children &&
Array.isArray(child.children)
) {
const result = findInChildLayers(child.children, layerId, child);
if (result) {
return result;
}
}
}
return null;
}
/**
* 简单查找图层(仅在顶级图层中查找,不递归子图层)
* @param {Array} layers 图层数组
* @param {string} layerId 要查找的图层ID
* @returns {Object|null} 找到的图层对象如果未找到返回null
*/
export function findLayer(layers, layerId) {
if (!layers || !Array.isArray(layers) || !layerId) {
return null;
}
return layers.find((layer) => layer && layer.id === layerId) || null;
}
/**
* 根据图层名称查找图层
* @param {Array} layers 图层数组
* @param {string} layerName 要查找的图层名称
* @param {boolean} recursive 是否递归查找子图层默认false
* @returns {Object|null} 找到的图层对象如果未找到返回null
*/
export function findLayerByName(layers, layerName, recursive = false) {
if (!layers || !Array.isArray(layers) || !layerName) {
return null;
}
for (const layer of layers) {
if (layer && layer.name === layerName) {
return layer;
}
// 如果需要递归查找且是组图层
if (
recursive &&
layer &&
(layer.type === "group" ||
layer.type === LayerType.GROUP ||
(layer.children && Array.isArray(layer.children)))
) {
const found = findLayerByName(layer.children, layerName, true);
if (found) {
return found;
}
}
}
return null;
}
/**
* 获取图层的完整路径(包含父图层信息)
* @param {Array} layers 图层数组
* @param {string} layerId 要查找的图层ID
* @returns {Array} 图层路径数组,从根图层到目标图层
*/
export function getLayerPath(layers, layerId) {
if (!layers || !Array.isArray(layers) || !layerId) {
return [];
}
function findPath(currentLayers, targetId, currentPath = []) {
for (const layer of currentLayers) {
if (!layer) continue;
const newPath = [...currentPath, layer];
if (layer.id === targetId) {
return newPath;
}
// 如果是组图层,递归查找
if (
layer.type === "group" ||
layer.type === LayerType.GROUP ||
(layer.children && Array.isArray(layer.children))
) {
const foundPath = findPath(layer.children, targetId, newPath);
if (foundPath.length > 0) {
return foundPath;
}
}
}
return [];
}
return findPath(layers, layerId);
}

View File

@@ -0,0 +1,204 @@
import { isArray } from "lodash-es";
/**
* 图层关联工具类
* 提供图层与画布对象关联管理的通用方法
*/
/**
* 构建单个图层与画布对象的关联关系
* @param {Object} layer 图层对象
* @param {Array} canvasObjects 画布对象数组
*/
export function buildLayerAssociations(layer, canvasObjects) {
if (!layer || !canvasObjects || !isArray(canvasObjects)) return;
// 处理单个fabricObject关联
if (layer.fabricObject) {
// 如果图层已经有关联的fabricObject确保它的layerId和layerName正确
layer.fabricObject =
canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
}
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组
layer.clippingMask =
canvasObjects.find((obj) => obj.id === layer.clippingMask.id) || null;
}
// 处理多个fabricObjects关联
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
layer.fabricObjects = layer.fabricObjects
.map((fabricObject) => {
// 确保每个fabricObject的layerId和layerName正确
const obj = canvasObjects.find((obj) => obj.id === fabricObject.id);
if (obj) {
obj.layerId = layer.id; // 确保对象的layerId正确
obj.layerName = layer.name; // 确保对象的layerName正确
return obj;
}
return null; // 如果没有找到对象返回null
})
.filter((obj) => obj !== null); // 过滤掉null值
}
}
/**
* 恢复对象与图层的关联关系
* @param {Object} layerManager 图层管理器实例
* @param {Array} canvasObjects 画布对象数组
*/
export function restoreObjectLayerAssociations(layers, canvasObjects) {
if (!layers || !canvasObjects || !isArray(canvasObjects)) return;
layers.forEach((layer) => {
buildLayerAssociations(layer, canvasObjects);
// 处理子图层
if (layer?.children?.length) {
restoreObjectLayerAssociations(layer.children, canvasObjects);
}
});
}
/**
* 为画布对象设置图层信息
* @param {Object} fabricObject 画布对象
* @param {Object} layer 图层对象
*/
export function setObjectLayerInfo(fabricObject, layer) {
if (!fabricObject || !layer) return;
fabricObject.layerId = layer.id;
fabricObject.layerName = layer.name;
}
/**
* 清除画布对象的图层信息
* @param {Object} fabricObject 画布对象
*/
export function clearObjectLayerInfo(fabricObject) {
if (!fabricObject) return;
delete fabricObject.layerId;
delete fabricObject.layerName;
}
/**
* 验证图层关联关系的完整性
* @param {Object} layerManager 图层管理器实例
* @param {fabric.Canvas} canvas 画布实例
* @returns {Object} 验证结果 { valid: boolean, issues: Array }
*/
export function validateLayerAssociations(layers, canvasObjects) {
const issues = [];
// 检查画布对象是否都有对应的图层
canvasObjects.forEach((obj) => {
if (obj.layerId) {
const layer = layers.find((l) => l.id === obj.layerId);
if (!layer) {
issues.push({
type: "orphaned_object",
objectId: obj.id,
layerId: obj.layerId,
message: `对象 ${obj.id} 关联的图层 ${obj.layerId} 不存在`,
});
}
} else {
issues.push({
type: "missing_layer_id",
objectId: obj.id,
message: `对象 ${obj.id} 缺少图层ID关联`,
});
}
});
// 检查图层是否都有对应的画布对象
layers.forEach((layer) => {
if (layer.fabricObject && layer.fabricObject.id) {
const obj = canvasObjects.find((o) => o.id === layer.fabricObject.id);
if (!obj) {
issues.push({
type: "missing_object",
layerId: layer.id,
objectId: layer.fabricObject.id,
message: `图层 ${layer.id} 关联的对象 ${layer.fabricObject.id} 不存在于画布中`,
});
}
}
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
layer.fabricObjects.forEach((fabricObj) => {
if (fabricObj.id) {
const obj = canvasObjects.find((o) => o.id === fabricObj.id);
if (!obj) {
issues.push({
type: "missing_object",
layerId: layer.id,
objectId: fabricObj.id,
message: `图层 ${layer.id} 关联的对象 ${fabricObj.id} 不存在于画布中`,
});
}
}
});
}
});
return {
valid: issues.length === 0,
issues,
};
}
/**
* 简化layers对象属性只保留必要的属性
* @param {Array} layers 图层数组 simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
*/
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 = {
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,
};
});
return layers;
}

View File

@@ -109,7 +109,7 @@
</div>
</template>
<script lang="ts">
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive, onMounted} from 'vue'
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive, onMounted, watch} from 'vue'
import { Https } from "@/tool/https";
import { useStore } from "vuex";
import sketchCategory from "@/component/HomePage/sketchCategory.vue";
@@ -165,8 +165,14 @@ export default defineComponent({
mannequinStyle:computed(()=>store.state.UserHabit.mannequinStyle),//风格列表
sexList:computed(()=>store.state.UserHabit.sex.value),//风格列表
ageGroupList:computed(()=>store.state.UserHabit.ageGroup),//风格列表
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
})
watch(()=>detailData.selectObject,(newValue,oldValue)=>{
detailData.mannequinData.sex = newValue.sex?newValue.sex:'Female'
detailData.mannequinData.style = newValue.style?newValue.style:''
detailData.mannequinData.ageGroup = newValue.ageGroup?newValue.ageGroup:''
},{immediate:true})
const getDetailListData = reactive({
total:0,
pageSize:10,

View File

@@ -71,6 +71,7 @@ export default defineComponent({
selectItem.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
},{immediate: true,})
watch(()=>detailData.frontBack?.body?.path,(newVal)=>{
let sacle = 0
const img = new Image();
img.onload = () => {
@@ -82,16 +83,22 @@ export default defineComponent({
let value = item.style[key]
if(typeof value !== 'number'){
value = value.replace('px','')
item.style[key] = value
}else{
item.style[key] = value*sacle+'px'
}
item.style[key] = value*sacle+'px'
// item.style[key] = value*sacle+'px'
}
for (const key in detailData.frontBack.back[index].style) {
if(key == 'zIndex')return
let value = detailData.frontBack.back[index].style[key]
if(typeof value !== 'number'){
value = value.replace('px','')
detailData.frontBack.back[index].style[key] = value
}else{
detailData.frontBack.back[index].style[key] = value*sacle+'px'
}
detailData.frontBack.back[index].style[key] = value*sacle+'px'
// detailData.frontBack.back[index].style[key] = value*sacle+'px'
}
});
};
@@ -415,7 +422,6 @@ export default defineComponent({
}
for (const key in data.instance.frontBack.back[index].style) {
if(key == 'zIndex')return
console.log(data.instance.frontBack.back[index].style[key].replace(/px/g,''))
data.instance.frontBack.back[index].style[key] = data.instance.frontBack.back[index].style[key].replace(/px/g,'')*sacle+'px'
}
});

View File

@@ -39,16 +39,6 @@
</div>
</div>
</div>
<!-- <input
class="search_input"
@input="ifMaximumLength"
:placeholder="(scene?.value == 'Slogan' && type_.type2 == 'Printboard')?isSloganHint:$t('Generate.inputContent1')"
:maxlength='inputShow?0:9999'
v-model="searchPictureName"
@keydown.enter="getgenerate()"
@click="inputFocus()"
@paste="onPaste"
/> -->
<textarea
class="textarea"
@input="ifMaximumLength"
@@ -82,7 +72,7 @@
<!-- <i v-show="!isTextarea" class="fi fi-br-expand" @click.stop="setTextareaShow"></i>
<i v-show="isTextarea" class="fi fi-bs-compress" @click.stop="setTextareaShow"></i> -->
</div>
<div class="input_box_btnBox sketch" v-else>
<div class="input_box_btnBox sketch" v-else >
<div class="upload_item" v-show="sketchboardList.length > 0">
<div
class="upload_file_item"
@@ -125,9 +115,9 @@
>
</a-upload>
</i>
<div :title="$t('Generate.style')">
<!-- <div :title="$t('Generate.style')">
<generalMenu :dataList="printModelList" :isCanvas="type_.type2 == 'Sketchboard'" @setprintModel="setprintModel" :item="printModel"></generalMenu>
</div>
</div> -->
</div>
<textarea
v-show="isTextarea"
@@ -141,18 +131,19 @@
<div class="generage_btn_box">
<div class="generage_btn started_btn" v-show="!isGenerate">
<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" v-show="
type_.type2 == 'Moodboard' ||
(type_.type2 == 'Printboard' && scene?.value == 'Pattern') ||
(type_.type2 == 'Sketchboard' && scene?.value == 'generate')" :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 class="icon iconfont icon-xiala" :class="{active:speedState}" @click.stop="openSpeed"></div>
<div class="content" v-show="speedState && scene?.value != 'extract'">
<div v-for="item in speedList" v-show="(type_.type2 == 'Moodboard' && item?.value != 'flux') || (type_.type2 == 'Sketchboard' && item?.value != 'flux') || type_.type2 == 'Printboard'" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
</div>
<div class="content" v-show="speedState && scene?.value == 'extract'">
<div v-for="item in extractList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
</div>
</div>
<div class="generage_btn started_btn" v-show="isGenerate && !remGenerate">
<i class="fi fi-br-loading" ></i>
</div>
<div class="generage_btn started_btn" v-show="remGenerate" @click="removeGenerate">
<div class="generage_btn started_btn" v-show="remGenerate" @click.stop="removeGenerate">
{{$t('Generate.Close')}}
</div>
@@ -182,19 +173,22 @@
@click="generageAdd(item)"
:class="[item.status != 'Success'?'hideEvents':'',item?.checked?'active':'']"
>
<img v-lazy="item.imgUrl" @click.stop="generageAdd(item)">
<sketchCategory v-if="type_.type2 == 'Sketchboard' || type_.type2 == 'Printboard'" :isSpread="type_.type2 == 'Printboard'" :disignTypeList="sketchCatecoryList" :generateList="fileList" :item="item" :driver__="driver__.driver" :driverClass="{'class1': type_.type2 == 'Sketchboard'?'Guide_1_13':'','class2':type_.type2 == 'Sketchboard'?'Guide_1_13_1':''}"></sketchCategory>
<img v-if="item?.imgUrl" v-lazy="item.imgUrl" @click.stop="generageAdd(item)">
<div v-else class="loading">
<a-spin size="large" ></a-spin>
</div>
<sketchCategory v-show="item?.imgUrl" v-if="type_.type2 == 'Sketchboard' || type_.type2 == 'Printboard'" :isSpread="type_.type2 == 'Printboard'" :disignTypeList="sketchCatecoryList" :generateList="fileList" :item="item" :driver__="driver__.driver" :driverClass="{'class1': type_.type2 == 'Sketchboard'?'Guide_1_13':'','class2':type_.type2 == 'Sketchboard'?'Guide_1_13_1':''}"></sketchCategory>
<div
v-show="item?.imgUrl"
class="delete_like_file_block left1"
:class="[driver__.driver?'hideEvents':'',]"
>
<i v-if="!item.like" class="fi fi-rr-heart" @click.stop="likeFile(item,'like')"></i>
<i v-if="!item.like" class="fi fi-rr-heart" @click="likeFile(item,'like')"></i>
<i v-else class="fi fi-sr-heart" :adminLike="!!item.like" @click.stop="likeFile(item,'noLike')"></i>
</div>
<div class="delete_like_file_block left" :class="[driver__.driver?'hideEvents':'']">
<div v-show="item?.imgUrl" class="delete_like_file_block left">
<i class="fi fi-bs-expand-arrows-alt" @click.stop="scaleImage(index)"></i>
</div>
<div class="delete_like_file_block" :title="t('LibraryPage.Delete')" @click.stop="deleteGenerate(index)">
<div v-show="item?.imgUrl" class="delete_like_file_block" :title="t('LibraryPage.Delete')" @click.stop="deleteGenerate(index)">
<span class="icon iconfont icon-shanchu operate_icon"></span>
</div>
</div>
@@ -294,6 +288,21 @@ export default defineComponent({
title:'Generate using Wanxiang',
label:'WX',
value:'wx',
},{
title:'',
label:'FLUX',
value:'flux',
},
],
extractList:[
{
title:'This method may produce slight discrepancies between the extracted line art and the original image.',
label:'High',
value:'',
},{
title:'Note: The extracted line art might have minor variations from the original.',
label:'FLUX',
value:'flux',
},
],
speedState:false,
@@ -311,8 +320,16 @@ export default defineComponent({
document.removeEventListener('click',openSpeed)
}
}
watch(()=>props.scene,(newVal,oldVal)=>{
if(newVal.value == 'extract'){
speed.speedData = speed.extractList[0]
}else{
speed.speedData = speed.speedList[0]
}
})
const setSpeed = (item:any)=>{
speed.speedData = item
speed.speedState = false
}
return {
userDetail,
@@ -471,6 +488,7 @@ export default defineComponent({
},
methods: {
generageAdd(data: any) {
if(!data?.imgUrl)return
data.type_ = this.type_;
data.type_.type1 = data.designType?data.designType:this.type_.type1
data.resData = JSON.parse(JSON.stringify(data))
@@ -487,12 +505,6 @@ export default defineComponent({
data.jsContent1 = this.t('uploadFile.jsContent1',{maxImg:maxImg})
this.store.commit("addGenerateMaterialFils", data);
// console.log(this.fileList);
let moodboard = this.store.state.UploadFilesModule.moodboardGenerateFiles
let sketch = this.store.state.UploadFilesModule.sketchGenerateFiles
let print = this.store.state.UploadFilesModule.printGenerateFiles
if((moodboard.length >= 2 || print.length >= 2 || sketch.length >= 2) && this.driver__.driver){
driverObj__.moveNext()
}
},
beforeUpload(file: any) {
const isJpgOrPng =
@@ -543,94 +555,109 @@ export default defineComponent({
})
},
getgenerate(){
if(this.scene?.value == 'extract'){
this.imageToSketch()
return
}
// if(this.scene?.value == 'extract'){
// this.imageToSketch()
// return
// }
this.isTextarea = false
this.isInputFocus = false
if(this.isGenerate)return
clearInterval(this.remGenerateTime)
if(this.searchPictureName){
let arr = this.searchPictureName.split(/\s+/).length
if(arr > 250){
message.info(
this.t('Generate.jsContent4')
);
return
let httpsUrl = Https.httpUrls.generatePrepare
let data
if(this.scene?.value == 'extract'){
httpsUrl = Https.httpUrls.imageToSketch
if((!this.printModel?.id && !this.printModel?.value) || !this.sketchboardList?.[0]?.id)return message.info(this.t('Generate.jsContent4'));
data = {
"elementId": this.sketchboardList[0].id,
gender:this.workspace.sex,
"style": this.printModel.value,
"styleImageId": this.printModel?.id?this.printModel?.id:'',
modelName:this.speedData.value,//为1就是Print
}
}else{
if(this.sketchboardList?.[0]?.imgUrl){
if(this.searchPictureName){
let arr = this.searchPictureName.split(/\s+/).length
if(arr > 250){
message.info(
this.t('Generate.jsContent4')
);
return
}
}else{
message.info(
this.t('Generate.jsContent5')
);
return
if(this.sketchboardList?.[0]?.imgUrl){
}else{
message.info(
this.t('Generate.jsContent5')
);
return
}
}
let level2Type = ''
let collectionElementId = ''
let base64 = ''
if(this.sketchboardList?.[0]){
collectionElementId = this.sketchboardList[0].id
if(this.sketchboardList[0].base64){
base64 = this.sketchboardList[0].imgUrl
}
}
let sloganText = ''
sloganText = this.searchPictureName
if(this.upload.level1Type == "Sketchboard"){
level2Type = this.sketchboardList?.[0]?.categoryValue?this.sketchboardList[0].categoryValue:''
if(this.workspace.styleName){
sloganText = `${this.workspace.styleName},${sloganText}`
}
}else if(this.upload.level1Type == "Printboard"){
level2Type = this.scene?.value
if(level2Type == 'Slogan' && this.searchPictureName == ''){
sloganText = this.isSloganHint
}else if(level2Type == 'Pattern'){
sloganText = `${this.printModel.value},${sloganText}`
}
if(!base64 && level2Type == 'Slogan'){
message.info(this.t('Generate.jsContent10'));
return
}
}
data = {
generateType:'text',
designType:'collection',
collectionElementId:collectionElementId,
level1Type:this.upload.level1Type,
level2Type:level2Type,
text:sloganText,
seed:this.searchPictureSeed,
userId:this?.userDetail?.userId,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
modelName:this.speedData.value,//为1就是Print
isTestUser:this.driver__.driver?false:this.isTest,
gender:this.workspace.sex,
sloganBase64:base64,
ageGroup:this.workspace.ageGroup
}
this.generateLevel2Type = data.level2Type
}
let level2Type = ''
let collectionElementId = ''
let base64 = ''
if(this.sketchboardList?.[0]){
collectionElementId = this.sketchboardList[0].id
if(this.sketchboardList[0].base64){
base64 = this.sketchboardList[0].imgUrl
}
}
let sloganText = ''
sloganText = this.searchPictureName
if(this.upload.level1Type == "Sketchboard"){
level2Type = this.sketchboardList?.[0]?.categoryValue?this.sketchboardList[0].categoryValue:''
if(this.workspace.styleName){
sloganText = `${this.workspace.styleName},${sloganText}`
}
}else if(this.upload.level1Type == "Printboard"){
level2Type = this.scene?.value
if(level2Type == 'Slogan' && this.searchPictureName == ''){
sloganText = this.isSloganHint
}else if(level2Type == 'Pattern'){
sloganText = `${this.printModel.value},${sloganText}`
}
if(!base64 && level2Type == 'Slogan'){
message.info(this.t('Generate.jsContent10'));
return
}
}
let data = {
generateType:'text',
designType:'collection',
collectionElementId:collectionElementId,
level1Type:this.upload.level1Type,
level2Type:level2Type,
text:sloganText,
seed:this.searchPictureSeed,
userId:this?.userDetail?.userId,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
modelName:this.speedData.value,//为1就是Print
isTestUser:this.driver__.driver?false:this.isTest,
gender:this.workspace.sex,
sloganBase64:base64,
}
this.generateLevel2Type = data.level2Type
this.isGenerate = true
this.remGenerateTime = setTimeout(()=>{
this.remGenerate = true
},10000)
Https.axiosPost(Https.httpUrls.generatePrepare, data).then(
// this.remGenerateTime = setTimeout(()=>{
// },10000)
Https.axiosPost(httpsUrl, data).then(
(rv) => {
// if(data.isTestUser){
// if(rv.leftUsageCount >= 1){
// message.warning(this.t('Generate.jsContent8',{num:rv.leftUsageCount,str:this.t('collectionModal.Moodboard')}));
// }else if(rv.leftUsageCount == 0){
// message.warning(this.t('Generate.jsContent9',{str:this.t('collectionModal.Moodboard')}));
// this.isGenerate = false
// return
// }
// }
if(this.scene?.value == 'extract'){
rv = {
uniqueId:[rv]
}
}
let rvData = rv.uniqueId.map((item:any)=>{
return{taskId:item,status:''}
})
this.remGenerate = true//出现取消按钮
this.fileList.unshift(...rvData)
this.setGenerate(rv.uniqueId)
}
).catch(res=>{
this.generateLevel2Type = ''
@@ -676,7 +703,9 @@ export default defineComponent({
if(element.status == 'Success'){
element.imgUrl = element.url
element.id_ = GO.id++
this.fileList.unshift(element)
let index = this.fileList.findIndex((item:any)=>item.taskId == element.taskId)
this.fileList[index] = element
// this.fileList.unshift(element)
data = data.filter((item:any) => item !== element.taskId);
if(this.type_.type2 == 'Sketchboard'){
this.sketchCatecoryList.forEach((itemCategory:any) => {
@@ -689,6 +718,10 @@ export default defineComponent({
element.categoryValue = this.scene?.value
element.category = this.scene?.name
}
}else if(element.status == 'Fail' || element.status == 'Invalid'){
data = data.filter((item:any) => item !== element.taskId);
this.fileList = this.fileList.filter((item:any) => item.taskId !== element.taskId);
message.info(this.t('Generate.everyTimeEffectPoor'));
}
});
if((data.length == 0)|| (rv.filter((item:any)=>item.status == 'Invalid').length ==data.length)){
@@ -718,7 +751,7 @@ export default defineComponent({
this.remGenerate = false
this.generateLevel2Type = ''
});
},1000)
},5000)
},
removeGenerate(){
//取消操作
@@ -737,8 +770,12 @@ export default defineComponent({
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
type: type
}
Https.axiosGet(Https.httpUrls.generateStopWaiting, {params:data}).then(
(rv) => {
this.generateProceedList.forEach((generateProceedListItem:any)=>{
this.fileList = this.fileList.filter((item:any) => generateProceedListItem.taskId!== item.taskId);
})
this.generateProceedList = []
}
).catch(res=>{
@@ -1044,6 +1081,13 @@ export default defineComponent({
pointer-events:none;
}
}
.loading{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
img {
// width: calc(10rem*1.2);
// height: calc(10rem*1.2);

View File

@@ -534,9 +534,9 @@ export default defineComponent({
imageStrength:(100 - imageStrength)/100,
}
productImgData.isProductimg = true
remPrductimgTime = setTimeout(()=>{
productImgData.remProductimg = true
},10000)
// remPrductimgTime = setTimeout(()=>{
// productImgData.remProductimg = true
// },10000)
let url = Https.httpUrls.toProduct
if(productimgMenu.value.value == 'Relight'){
url = Https.httpUrls.relight
@@ -544,6 +544,7 @@ export default defineComponent({
productImgData.isShowMark = true
Https.axiosPost(url, data).then(
(rv) => {
productImgData.remProductimg = true
productImgData.isShowMark = false
let arr:any = []
rv.forEach((item:any)=>{

View File

@@ -31,7 +31,12 @@
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">{{$t('ProductImg.MagicTools')}}</span>
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'Relight'">{{$t('ProductImg.relightingTool')}}</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>Selection Function</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction" style="margin-bottom: 1rem;">
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.Similarity')}}</span>
</div>
@@ -45,10 +50,10 @@
</a-slider>
<input type="number" readonly v-model="productimgSimilarity">
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.RelightDirection')}}</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
<!-- <a-slider class="system_silder"
v-model:value="similarity"
@afterChange="setSimilarity"
@@ -58,10 +63,10 @@
</a-slider> -->
<a-select style="width: 100%;" v-model:value="productimgRelightDirection" :options="productimgRelightDirectionList"></a-select>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.Highlight')}}</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_similarity">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
<a-slider class="system_silder"
v-model:value="productimgBrightenValue"
:tooltipVisible="false"
@@ -92,7 +97,22 @@
></textarea>
</div>
</div>
<div class="productImg_content_item_generate_btn input_border">
<div class="generage_btn_box" style="margin-left: auto;">
<div class="generage_btn started_btn" v-show="!productimgIsProductimg">
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg()"></i>
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType != 'Relight'" :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>
</div>
<div class="generage_btn started_btn" v-show="productimgIsProductimg && !productimgRemProductimg">
<i class="fi fi-br-loading" ></i>
</div>
<div class="generage_btn started_btn" v-show="productimgRemProductimg" @click="removeProductimg">
{{$t('Generate.Close')}}
</div>
</div>
<!-- <div class="productImg_content_item_generate_btn input_border">
<div class="input_box">
<div v-show="!productimgIsProductimg" class="generage_btn started_btn" @click.stop="getPrductimg">
{{ $t('Generate.Generate') }}
@@ -104,7 +124,7 @@
{{$t('Generate.Close')}}
</div>
</div>
</div>
</div> -->
</div>
</div>
<div class="scaleImage_content_imgBox" :class="{active:isComparison}">
@@ -189,6 +209,55 @@ export default defineComponent({
productimgRelightDirection:props.productData.RelightDirection,
productimgRelightDirectionList:props.productData.RelightDirectionList,
})
let speed = reactive({
speedList:[
] as any,
speedTypeList:{
poseTransfer:[
{
title:'Generate high-quality images',
label:'High',
value:'',
},{
title:'Generate using Wanxiang',
label:'WX',
value:'wx',
},
],
toPorductImg:[
{
title:'Generate with high quality',
label:'High',
relightLabel:'Relight',
value:'',
},{
title:'',
label:'FLUX',
relightLabel:'Edit',
value:'flux',
},
]
},
speedState:false,
speedData:{
title:'Generate high-quality images',
relightLabel:'Relight',
label:'High',
value:'',
},
})
const openSpeed = ()=>{
speed.speedState = !speed.speedState
if(speed.speedState){
document.addEventListener('click',openSpeed)
}else{
document.removeEventListener('click',openSpeed)
}
}
const setSpeed = (item:any)=>{
speed.speedData = item
speed.speedState = false
}
let scaleImage: any = ref(false);
let isShowMark = ref(false)
let loadingShow = ref(false)
@@ -224,9 +293,9 @@ export default defineComponent({
}
productimg.productimgIsProductimg = true
remPrductimgTime = setTimeout(()=>{
productimg.productimgRemProductimg = true
},10000)
// remPrductimgTime = setTimeout(()=>{
// productimg.productimgRemProductimg = true
// },10000)
let url = Https.httpUrls.relight
if(scaleImageList.value[scaleImageIndex.value]?.resultType == 'ToProductImage'){
url = Https.httpUrls.toProduct
@@ -234,6 +303,7 @@ export default defineComponent({
isShowMark.value = true
Https.axiosPost(url, data).then(
(rv) => {
productimg.productimgRemProductimg = true
isShowMark.value = false
scaleImageList.value[scaleImageIndex.value].imgUrl = '/image/loading.gif'
let arr:any = []
@@ -381,6 +451,9 @@ export default defineComponent({
return {
t,
...toRefs(productimg),
...toRefs(speed),
openSpeed,
setSpeed,
scaleImage,
isShowMark,
loadingShow,
@@ -427,6 +500,12 @@ export default defineComponent({
this.scaleImageIndex = index
if(dialogueIndex)this.robotAssits = dialogueIndex
// let scaleImageList = this.store.state.UploadFilesModule.moodboard
if(this.scaleImageList[index].resultType == "PoseTransfer"){
this.speedList = this.speedTypeList.poseTransfer
}else{
this.speedList = this.speedTypeList.toPorductImg
}
this.speedData = JSON.parse(JSON.stringify(this.speedList[0]))
document.addEventListener('keydown',this.setKeydown)
},
cancelDsign(){

View File

@@ -81,11 +81,13 @@ export default defineComponent({
height: auto;
max-height: 80vh;
position: absolute;
width: max-content;
video{
width: 100%;
max-height: 80vh;
height: 100%;
object-fit: contain;
width: max-content;
}
.general_video_btn{
color: #fff;

View File

@@ -24,7 +24,7 @@
</div>
</div>
<div class="modal_title_text">
<div>Create Cloud Generation Tasks</div>
<div>Create Batch Generation Tasks</div>
</div>
<div class="allUserPoeration_center admin_page">
<div class="admin_state_item">
@@ -52,12 +52,24 @@
placeholder="Please select"
:options="objectList"
@search="getHistoryProjectList"
@change="changeProject"
>
<template #option="{ value: val, label, icon,updateTime }">
<span :title="updateTime.replace('T', ' ')">{{ label }}</span>
</template>
</a-select>
</div>
<div class="admin_state_item">
<span>Name <span>*</span></span>
<input
v-model="porjectName"
:placeholder="placeholder"
@focus="focus"
@blur="blur"
type="text"
style="width: 200px"
/>
</div>
<div class="admin_state_item" v-show="buildType">
<span>Quantity <span>*</span></span>
<input
@@ -72,14 +84,17 @@
<div v-show="buildType == 'TO_PRODUCT_IMAGE'" class="admin_state_item ">
<span>{{$t('ProductImg.Similarity')}}</span>
<div class="sliderAndImput" style="width: 200px">
<a-slider class="system_silder"
<!-- <a-slider class="system_silder"
v-model:value="similarity"
range
:step="5"
>
</a-slider>
<!-- <input type="number" readonly v-model="similarity"> -->
</a-slider> -->
<div style="display: flex;">
<input type="number" readonly v-model="similarity[0]">
<div style="margin: 0 1rem;">-</div>
<input type="number" readonly v-model="similarity[1]">
</div>
</div>
</div>
<div v-show="buildType == 'RELIGHT'" class="admin_state_item ">
@@ -117,12 +132,18 @@
</div>
</div> -->
<div class="productImg_content_item_imgBox generalScroll upload_item" v-if="buildType && buildType != 'SERIES_DESIGN' && buildType != 'SINGLE_DESIGN'" v-mousewheel>
<div class="content_item_imgBox_itemImg" v-for="item in uploadElement" :key="item">
<div
class="imgBox"
@click="()=>item.isChecked = !item.isChecked"
>
<img :class="[item?.isChecked?'active':'']" :src="item?.url" class="upload_img"/>
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
</div>
</div>
<div class="content_item_imgBox_itemImg" v-for="(file, index) in fileList" :key="file">
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" >
<a-spin
:indicator="indicator"
tip="Uploading..."
/>
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" style="display: flex;align-items: center;">
<a-spin size="large" />
</div>
<div
class="imgBox"
@@ -217,9 +238,10 @@ export default defineComponent({
exhibitionImgList:[],//选择的图片
projectData:null,//批量id
objectList:[],
porjectName:'',//任务名字
//toProduct
generateText:'',//输入的内容
similarity:[30,60],
similarity:[20,40],
brightenValue:1,//亮度
relightDirection:'Right Light',//打光方向
relightDirectionList:[
@@ -238,13 +260,43 @@ export default defineComponent({
}
],
fileList:[],
uploadElement:[],
placeholder:'',
})
const getUploadElement = ()=>{
operations.loadingShow = true
let value = {
id:operationsData.projectData,
moduleList:['uploadElement']
}
operationsData.placeholder = 'Batch_' + setPlaceholder()
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
operationsData.uploadElement = rv.uploadElement
operations.loadingShow = false
}).catch((err)=>{
operations.loadingShow = false
})
}
let init = (projectData,buildType)=>{
operations.operationsModal = true
if(projectData?.id)operationsData.projectData = {label:projectData.name,value:projectData.id}
clearData()
if(projectData?.id){
operationsData.projectData = {label:projectData.name,value:projectData.id}
getUploadElement()
}
if(buildType.value)operationsData.buildType = buildType
}
const clearData = ()=>{
operationsData.porjectName = ''
operationsData.generateText = ''
operationsData.similarity = [20,40]
operationsData.brightenValue = 1
operationsData.fileList = []
operationsData.uploadElement = []
}
const changeProject = ()=>{
getUploadElement()
}
const changeBuildType = ()=>{
// operationsData.exhibitionImgList = []
operationsData.projectData = null
@@ -255,15 +307,20 @@ export default defineComponent({
})
getHistoryProjectList()
}
const getGenerateCloudImgList = ()=>{
const getGenerateCloudImgList = (type)=>{
let list = []
if(operationsData.buildType == 'SINGLE_DESIGN'|| operationsData.buildType == 'SERIES_DESIGN')return list
let selectList = operationsData.fileList.filter((item)=>item.isChecked)
if(type == 'SINGLE_DESIGN'|| type == 'SERIES_DESIGN')return list
let selectList = []
let fileList = operationsData.fileList.filter((item)=>item.isChecked)
let uploadElement = operationsData.uploadElement.filter((item)=>item.isChecked)
if(fileList)selectList.push(...fileList)
if(uploadElement)selectList.push(...uploadElement)
selectList.forEach((item)=>{
let obj = {
}
if(operationsData.buildType == 'POSE_TRANSFER'){
if(type == 'POSE_TRANSFER'){
obj = {
poseId:1,
productImage:getMinioUrl(item.imgUrl)
@@ -271,7 +328,7 @@ export default defineComponent({
}else{
obj = {
elementId:item.id,
elementType:item.type
elementType:item.type||'ProductElement'
}
}
list.push(obj)
@@ -337,10 +394,11 @@ export default defineComponent({
"buildType": buildTypeCorresponding[operationsData.buildType],
nums: operationsData.numberOfImages,
projectId: operationsData.projectData,
name:operationsData.porjectName || operationsData.projectData,
//productimg
toProductImage:{
prompt:operationsData.generateText,//输入的内容
toProductImageVOList:(operationsData.buildType == 'TO_PRODUCT_IMAGE' || operationsData.buildType == 'RELIGHT')?getGenerateCloudImgList():[],//选择的图片
toProductImageVOList:getGenerateCloudImgList(operationsData.buildType),//选择的图片
// toProductImageVOList:getPorductImg(),//选择的图片
projectId: operationsData.projectData,
direction:operationsData.relightDirection,//打光方向
@@ -351,7 +409,7 @@ export default defineComponent({
},
//poseTransform
// poseTransform:getPoseTransformData(),
poseTransform:operationsData.buildType == 'POSE_TRANSFER'?getGenerateCloudImgList():[],
poseTransform:operationsData.buildType == 'POSE_TRANSFER'?getGenerateCloudImgList('POSE_TRANSFER'):[],
private: operationsData.projectData,
ToProductImageDTO: operationsData.projectData,
}
@@ -375,7 +433,7 @@ export default defineComponent({
// if(data.poseTransform.length == 0)return message.warning("You must first generate results in the 'To Product Image' module before you can use the 'Transfer Pose' cloud generation feature.")
}
if(operationsData.buildType == 'DESIGN' && !operationsData.projectData)return message.warning('Please select a project')
if(!data.buildType || !data.nums || (operationsData.buildType == 'DESIGN' && !operationsData.projectData))return message.warning('Please check the input box marked with *')
if(!data.buildType || !data.nums || !data.name || (operationsData.buildType == 'DESIGN' && !operationsData.projectData))return message.warning('Please check the input box marked with *')
operations.loadingShow = true
Https.axiosPost(Https.httpUrls.designCloud, data).then(
(rv) => {
@@ -458,11 +516,25 @@ export default defineComponent({
bor = false
}
}
const setPlaceholder = ()=>{
if(!operationsData.projectData)return ''
let index = operationsData.objectList.findIndex(item => item.id === operationsData.projectData)
return operationsData.objectList[index].name
}
const focus = ()=>{
if(operationsData.porjectName)return
operationsData.porjectName = operationsData.placeholder
}
const blur = ()=>{
if(operationsData.porjectName != operationsData.placeholder)return
operationsData.porjectName = ''
}
return {
...toRefs(operations),
...toRefs(operationsData),
cancelDsign,
init,
changeProject,
focus,
blur,
setOk,
@@ -658,13 +730,17 @@ export default defineComponent({
// border-radius: 1.6rem;
flex: 1;
}
> input{
border-radius: 1.6rem;
width: 4rem;
margin-left: 1rem;
height: 100%;
border-radius: 1rem;
>div{
input{
border-radius: 1.6rem;
width: 5rem;
padding: 4px 11px 4px;
margin-left: 1rem;
height: 100%;
text-align: center;
}
}
}
}
}

View File

@@ -47,17 +47,35 @@
<div class="content">
<tr v-for="(row, index) in contentList" :key="index">
<td v-for="header in cloudTiltleList" :key="header.value">
<span v-show="header.value != 'operation'">
<div v-if="header.value != 'operation' && header.value != 'name'">
{{header?.fun?header.fun(row[header.value]) : row[header.value]}}
</div>
<div v-if="header.value == 'name'">
<div v-if="row.id == renameId" class="rename">
<input type="text" v-model="renameText">
<i class="fi fi-br-check" @click="submitRename(row)"></i>
</div>
<div v-else>
{{header?.fun?header.fun(row[header.value]) : row[header.value]}}
</div>
</div>
<span style="color: #007EE5; cursor: pointer; margin-right: 1rem;" v-show="header.value == 'operation'" @click="setRename(row)">
Rename
</span>
<span style="color: #007EE5; cursor: pointer;" v-show="header.value == 'operation'" @click="detailIamge(row)">
<span style="color: #007EE5; cursor: pointer; margin-right: 1rem;" v-show="header.value == 'operation'" @click="detailIamge(row)">
Review
</span>
<span style="color: #007EE5; cursor: pointer;" v-show="header.value == 'operation'" @click="deleteRom(row)">
Delete
</span>
</td>
</tr>
</div>
<a-pagination style="text-align: center;" @change="pagination" v-model:current="currentPage" :total="total" show-less-items />
</div>
<div class="mark_loading" v-show="loadingShow">
<a-spin size="large" />
</div>
<createCloud ref="createCloud" :cloudList="generateList.seriesDesign" @getContentList="submitGetContentList"></createCloud>
</div>
</template>
@@ -159,6 +177,9 @@ export default defineComponent({
},
cloudTiltleList:[
{
name:'Task Name',
value:'name',
},{
name:'Task type',
value:'buildType',
fun:(value:any)=>{
@@ -213,6 +234,9 @@ export default defineComponent({
] as any,
objectList:[],
isGetContentList:false as any,
renameId:-1 as any,
renameText:'',
loadingShow:false,
})
const dataDom = reactive({
createCloud,
@@ -259,7 +283,13 @@ export default defineComponent({
store.commit('setCloudList',{str:'relight',list:rv.relight})
router.push(`/home/tools?tools=${item.buildType}&id=${item.projectId}&source=batch`)
}else if(item.buildType == 'poseTransfer'){
store.commit('setCloudList',{str:'poseTransfer',list:rv.poseTransfer})
let list = {
list:rv.poseTransfer,
str:'add',
index:-1,
}
store.commit("setPoseTransfer", list);
// store.commit('setCloudList',{str:'poseTransfer',list:rv.poseTransfer})
router.push(`/home/tools?tools=${item.buildType}&id=${item.projectId}&source=batch`)
}
// if(rv.design && rv.design.length > 0){
@@ -281,6 +311,7 @@ export default defineComponent({
}
const pagination = ()=>{
data.isGetContentList = true
data.renameId = -1
getContentList()
}
let time = null as any
@@ -290,7 +321,7 @@ export default defineComponent({
let value = {
page:data.currentPage,
size:data.pageSize,
projectId: data.projectData?.value,
projectId: data.projectData?.value?data.projectData?.value:'',
}
Https.axiosPost(Https.httpUrls.cloudPage,value).then((rv)=>{
data.contentList = rv.content
@@ -342,6 +373,40 @@ export default defineComponent({
const handleChange = (event:any,value:any)=>{
data.createData = value
}
const setRename = (item:any)=>{
data.renameId = item.id
data.renameText = item.name
}
const submitRename = (item:any)=>{
data.renameId = -1
data.loadingShow = true
let value = {
id:item.id,
name:data.renameText,
}
Https.axiosPost(Https.httpUrls.cloudTaskNameUpdate,value).then((rv)=>{
data.loadingShow = false
data.renameText = ''
data.isGetContentList = true
getContentList()
}).catch((err)=>{
data.loadingShow = false
})
}
const deleteRom = (item:any)=>{
let value = {
id:item.id
}
Https.axiosPost(Https.httpUrls.cloudTaskDelete,value).then((rv)=>{
data.loadingShow = false
data.isGetContentList = true
getContentList()
}).catch((err)=>{
data.loadingShow = false
})
}
onBeforeUnmount(()=>{
data.isGetContentList = false
})
@@ -366,6 +431,9 @@ export default defineComponent({
handleChange,
getHistoryProjectList,
pagination,
setRename,
submitRename,
deleteRom,
}
},
provide() {
@@ -452,7 +520,31 @@ export default defineComponent({
text-align: center;
width: calc(100% / 4);
line-height: 4.6rem;
font-size: 2.2rem;
font-size: 1.6rem;
display: flex;
align-items: center;
justify-content: center;
.rename{
display: flex;
align-items: center;
input{
height: 100%;
padding: .8rem;
width: 12rem;
}
> i{
margin-left: 1rem;
cursor: pointer;
width: 3rem;
height: 3rem;
background: #000;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 2rem;
}
}
}
}
}

View File

@@ -80,6 +80,7 @@ import { useStore } from "vuex";
import { Modal,message,Upload,CascaderProps } from 'ant-design-vue';
import { useI18n } from 'vue-i18n'
import { setCookie, getCookie, WriteCookie } from "@/tool/cookie";
import { useRouter,useRoute } from 'vue-router'
export default defineComponent({
components:{
},
@@ -88,6 +89,7 @@ export default defineComponent({
emits:['chatChange'],
setup(props,{emit}) {
const store = useStore();
const route = useRoute()
const data = reactive({
chatContent:'',
openChat:true,
@@ -107,6 +109,7 @@ export default defineComponent({
watch(()=>data.selectObject.id,(newValue,oldValue)=>{
if(newValue && (data.selectObject.httpType == 'SERIES_DESIGN' || data.selectObject.httpType == 'SINGLE_DESIGN')){
data.chatList = []
if(route.query?.create)return
nextTick(()=>{
getChatHistory(newValue)
})
@@ -146,7 +149,7 @@ export default defineComponent({
// data.chatList[data.chatList.length-1].content.message+=JSON.parse(event.data).content
// }
const container = dataDom.chatBox;
container.scrollTop = container.scrollHeight;
if(container?.scrollHeight)container.scrollTop = container.scrollHeight;
const eventData = JSON.parse(event.data)
if(eventData.type == 'text'){
@@ -155,6 +158,11 @@ export default defineComponent({
data.chatList[data.chatList.length-1].content.think+=eventData.content
}else if(eventData.type == "tools_response"){
let nameList = ['moodboard','printboard','sketchboard','generate_color_code']
let nameData = {
moodboard:'moodBoard',
printboard:'printBoard',
sketchboard:'sketchBoard',
} as any
let getData = ''
if(nameList.indexOf(eventData.tools_name) > -1){
if(data.chatList[data.chatList.length - 1].content.message)data.chatList.push({content:{message:''},role:'system'})
@@ -163,16 +171,14 @@ export default defineComponent({
getData = 'colorboard'
}else{
data.chatList[data.chatList.length-1].content.img = JSON.parse(JSON.parse(event.data).content).receiveCollectionElementList
getData = eventData.tools_name
getData = nameData[eventData.tools_name]
}
data.chatList.push({content:{message:''},role:'system'})
}else{
}else if(eventData.tools_name == 'design_control_signal'){
emit('chatChange',{type:eventData.tools_name,design:true})
}
emit('chatChange',{type:eventData.type,module:getData})
}else if(eventData.type == "design_control_signal"){
emit('chatChange',{type:eventData.type,design:true})
}
//emit('chatChange',{type:JSON.parse(event.data).status})
};

View File

@@ -11,7 +11,7 @@
:closable="false"
:mask="true"
:keyboard="false"
:destroyOnClose="true"
:destroyOnClose="false"
:zIndex="1000"
>
<div class="generalModel_btn">
@@ -23,18 +23,33 @@
</svg>
</div>
</div>
<div class="collection_title">
<div class="collection_title_text">
<div v-show="collectionStep === 1">{{ $t('collectionModal.Moodboard') }}</div>
<div v-show="collectionStep === 2">{{ $t('collectionModal.Printboard') }}</div>
<div v-show="collectionStep === 3">{{ $t('collectionModal.Colorboard') }}</div>
<div v-show="collectionStep === 4">{{ $t('collectionModal.Mannquinboard') }}</div>
<div v-show="collectionStep === 5">{{ $t('collectionModal.Sketchboard') }}</div>
<!-- <div v-show="collectionStep === 5">Markets Sketch</div> -->
<!-- <div class="collection_title_text_intro" v-show="collectionStep === 1">{{ $t('collectionModal.MoodCollection') }}</div>
<div class="collection_title_text_intro" v-show="collectionStep === 2">{{ $t('collectionModal.PrinCollection') }}</div>
<div class="collection_title_text_intro" v-show="collectionStep === 3">{{ $t('collectionModal.ColorCollection') }}</div>
<div class="collection_title_text_intro" v-show="collectionStep === 4">{{ $t('collectionModal.SketchCollection') }}</div>
<div class="collection_title_text_intro" v-show="collectionStep === 4">{{ $t('collectionModal.SketchCollection') }}</div> -->
</div>
</div>
<div class="designOpenrtion_content">
<!-- <div class="modal_title_text">
<div>Setting</div>
</div> -->
<div class="collectionBox">
<MoodboardUpload ref="moodBoard" v-if="openType == 'moodBoard' || collectionStep == 1"></MoodboardUpload>
<PrintboardUpload ref="printBoard" v-if="openType == 'printBoard' || collectionStep == 2"></PrintboardUpload>
<ColorboardUpload ref="colorBoard" v-if="openType == 'colorBoard' || collectionStep == 3"></ColorboardUpload>
<SketchboardUpload ref="sketchBoard" v-if="openType == 'sketchBoard' || collectionStep == 4"></SketchboardUpload>
<mannequin ref="mannequin" v-if="openType == 'mannequin' || collectionStep == 5"></mannequin>
<MoodboardUpload ref="moodBoard" v-show="openType == 'moodBoard' || collectionStep == 1"></MoodboardUpload>
<PrintboardUpload ref="printBoard" v-show="openType == 'printBoard' || collectionStep == 2"></PrintboardUpload>
<ColorboardUpload ref="colorBoard" v-show="openType == 'colorBoard' || collectionStep == 3"></ColorboardUpload>
<mannequin ref="mannequin" v-show="openType == 'mannequin' || collectionStep == 4"></mannequin>
<SketchboardUpload ref="sketchBoard" v-show="openType == 'sketchBoard' || collectionStep == 5"></SketchboardUpload>
</div>
<div class="collection_page">
<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-else class="fi fi-rr-check Guide_1_14" @click.stop="cleardata()"></i>
@@ -47,11 +62,12 @@
</a-modal>
</template>
<script lang="ts">
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive} from 'vue'
import { defineComponent,computed,ref,provide,nextTick,inject,toRefs, reactive, onBeforeMount} from 'vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { Https } from "@/tool/https";
import { useStore } from "vuex";
import { useI18n } from 'vue-i18n'
import { rgbToHsv, dataURLtoBlob } from "@/tool/util";
import { init } from 'echarts/core';
import MoodboardUpload from './collection/MoodboardUpload.vue';
import PrintboardUpload from './collection/PrintboardUpload.vue';
@@ -64,6 +80,10 @@ export default defineComponent({
MoodboardUpload,PrintboardUpload,ColorboardUpload,SketchboardUpload,mannequin,
},
props:{
isNext:{
type:Boolean,
default:true,
},
},
emits:['getHistory'],
setup(props,{emit}) {
@@ -74,6 +94,8 @@ export default defineComponent({
openType:'',
collectionStep:1,
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
createProbject:inject('createProbject') as any
})
let driver__:any = computed(()=>{
return store.state.Guide.guide
@@ -83,8 +105,8 @@ export default defineComponent({
moodBoard:null as any,
printBoard:null as any,
colorBoard:null as any,
sketchBoard:null as any,
mannequin:null as any,
sketchBoard:null as any,
}) as any
const init = (value:any)=>{
data.habitSetStyle = true
@@ -95,14 +117,16 @@ export default defineComponent({
dataDom[value].openSetData()
})
}
let cleardata = ()=>{
let cleardata = async ()=>{
data.habitSetStyle = false
if(data.collectionStep == 3)await getPantongName()
data.collectionStep = 1
if(data.openType)store.dispatch('setAllBoardData',{type:data.openType})
saveProject(data.openType)
}
const saveProject = (str:any)=>{
const saveProject = async (str:any)=>{
if(str == 'design')return
if(!data.selectObject.id && data.createProbject)await data.createProbject()
let value:any = {
projectId:data.selectObject.id,
}
@@ -114,11 +138,49 @@ export default defineComponent({
})
})
}
let lastStep = ()=>{
let getPantongName = ()=>{
let colorBoards = store.state.UploadFilesModule.colorBoards;
if(!colorBoards || colorBoards?.length == 0) return
data.isShowMark = true
let value: any = [];
for (let v of colorBoards) {
let color: any = [v.rgbValue.r, v.rgbValue.g, v.rgbValue.b];
let hsv = rgbToHsv(color);
v.hsv = hsv[0] + hsv[1] + hsv[2];
value.push({
h: hsv[0],
s: hsv[1],
v: hsv[2],
});
}
return new Promise((resolve: any, reject: any) => {
Https.axiosPost(Https.httpUrls.getRgbByHsvBatch, value)
.then((rv: any) => {
if (rv) {
rv.forEach((ele: any, index: number) => {
colorBoards[index].id = ele.id;
colorBoards[index].tcx = ele.tcx;
colorBoards[index].name = ele.name;
});
store.commit("setColorboardList", colorBoards);
resolve();
}
data.isShowMark = false
})
.catch((res) => {
reject();
data.isShowMark = false
});
});
}
let lastStep = async ()=>{
if(data.collectionStep == 3)await getPantongName()
data.collectionStep = data.collectionStep - 1
setOpenSetData()
}
let nextStep = ()=>{
let nextStep = async ()=>{
if(data.collectionStep == 3)await getPantongName()
data.collectionStep = data.collectionStep + 1
setOpenSetData()
}
@@ -150,6 +212,42 @@ export default defineComponent({
</script>
<style lang="less" scoped>
.collectionModal{
:deep(.ant-modal-body){
padding-top: 10rem;
}
.collection_title{
top: calc(4rem*1.2);
display: flex;
font-size: var(--aida-fsize2);
font-weight: 900;
color: rgba(0,0,0,.65);
z-index: 999;
align-items: center;
width: calc(35rem*1.2);
justify-content: space-between;
.collection_progress{
width: calc(8rem*1.2);
height: calc(8rem*1.2);
>div{
width: 100% !important;
height: 100% !important;
:deep(.ant-progress-inner){
width: 100% !important;
height: 100% !important;
}
}
}
.collection_title_text{
// margin-right: calc(4rem*1.2);
}
.collection_title_text_intro{
font-size: var(--aida-fsize1-4);
font-weight: 400;
color: rgba(0,0,0,.45);
}
}
:deep(.designOpenrtion_content){
display: flex;
flex-direction: column;

View File

@@ -560,7 +560,7 @@ export default defineComponent({
level2Type:'',
designType:'',
}
let arr = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles)) || []
let arr = this.store.state.UploadFilesModule.allBoardData.printboardFiles?JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles)) : []
let setboard = {
generate:[] as any,
material:[] as any,

View File

@@ -31,6 +31,12 @@
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">{{$t('ProductImg.MagicTools')}}</span>
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'Relight'">{{$t('ProductImg.relightingTool')}}</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>Selection Function</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction" style="margin-bottom: 1rem;">
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.Similarity')}}</span>
</div>
@@ -44,23 +50,16 @@
</a-slider>
<input type="number" readonly v-model="productimgSimilarity">
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.RelightDirection')}}</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction">
<!-- <a-slider class="system_silder"
v-model:value="similarity"
@afterChange="setSimilarity"
:tooltipVisible="false"
:step="5"
>
</a-slider> -->
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
<a-select style="width: 100%;" v-model:value="productimgRelightDirection" :options="productimgRelightDirectionList"></a-select>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.Highlight')}}</span>
</div>
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_similarity">
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
<a-slider class="system_silder"
v-model:value="productimgBrightenValue"
:tooltipVisible="false"
@@ -109,7 +108,7 @@
<div class="generage_btn_box" style="margin-left: auto;">
<div class="generage_btn started_btn" v-show="!generateSuccess.productimgIsProductimg">
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg()"></i>
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType == 'PoseTransfer'" :class="{active:speedState}" @click.stop="openSpeed"></div>
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType != 'Relight'" :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>
@@ -237,19 +236,37 @@ setup(props:any,{emit}) {
})
let speed = reactive({
speedList:[
{
title:'Generate high-quality images',
label:'High',
value:'',
},{
title:'Generate using Wanxiang',
label:'WX',
value:'wx',
},
],
] as any,
speedTypeList:{
poseTransfer:[
{
title:'Generate high-quality images',
label:'High',
value:'',
},{
title:'Generate using Wanxiang',
label:'WX',
value:'wx',
},
],
toPorductImg:[
{
title:'Generate with high quality',
label:'High',
relightLabel:'Relight',
value:'',
},{
title:'',
label:'FLUX',
relightLabel:'Edit',
value:'flux',
},
]
},
speedState:false,
speedData:{
title:'Generate high-quality images',
relightLabel:'Relight',
label:'High',
value:'',
},
@@ -264,6 +281,7 @@ setup(props:any,{emit}) {
}
const setSpeed = (item:any)=>{
speed.speedData = item
speed.speedState = false
}
let scaleImage: any = ref(false);
let isShowMark = ref(false)
@@ -316,15 +334,17 @@ setup(props:any,{emit}) {
direction:productimg.productimgRelightDirection,
prompt:productimg.productimgSearchName,
toProductImageVOList:[obj],
modelName:speed.speedData.value,
brightenValue:productimg.productimgBrightenValue,
projectId:productimg.selectObject.id,
imageStrength:(100 - imageStrength)/100,
ageGroup:productimg.selectObject.ageGroup
}
// return
productimg.generateSuccess.productimgIsProductimg = true
productimg.generateSuccess.remPrductimgTime = setTimeout(()=>{
productimg.generateSuccess.productimgRemProductimg = true
},10000)
// productimg.generateSuccess.remPrductimgTime = setTimeout(()=>{
// productimg.generateSuccess.productimgRemProductimg = true
// },10000)
let url = Https.httpUrls.relight
if(scaleImageList.value[scaleImageIndex.value]?.resultType == 'ToProductImage'){
url = Https.httpUrls.toProduct
@@ -337,6 +357,7 @@ setup(props:any,{emit}) {
productimg.generateSuccess.isShowMark = true
Https.axiosPost(url, data).then(
(rv) => {
productimg.generateSuccess.productimgRemProductimg = true
productimg.generateSuccess.isShowMark = false
scaleImageList.value[scaleImageIndex.value].imgUrl = '/image/loading.gif'
let arr:any = []
@@ -484,7 +505,8 @@ setup(props:any,{emit}) {
oldId:productimg.generateSuccess.id,
status:productimg.generateSuccess.status,
listType:productimg.generateSuccess.listType,
isIndex:productimg.generateSuccess.isIndex
isIndex:productimg.generateSuccess.isIndex,
userLikeSortId:productimg.generateSuccess.userLikeSortId
}
emit('addGenerateImg',data)
@@ -565,7 +587,8 @@ methods: {
this.generateSuccess.isIndex = index
}
// this.
this.generateSuccess.userLikeSortId = list[index].userLikeSortId
this.generateSuccess.productimgIsProductimg = !!this.generateSuccess.productimgIsProductimg
this.generateSuccess.productimgRemProductimg = !!this.generateSuccess.productimgRemProductimg
this.generateSuccess.isShowMark = !!this.generateSuccess.isShowMark
@@ -595,6 +618,12 @@ methods: {
this.scaleImage = true
this.isGenerate = false
this.scaleImageList = list
if(this.scaleImageList[index].resultType == "PoseTransfer"){
this.speedList = this.speedTypeList.poseTransfer
}else{
this.speedList = this.speedTypeList.toPorductImg
}
this.speedData = JSON.parse(JSON.stringify(this.speedList[0]))
// if(this.scaleImageList[index]?.resultType == 'ToProductImage')this.scaleImageList[index].sourceUrl = this.scaleImageList[index].imgUrl
this.scaleImageIndex = index
if(dialogueIndex)this.robotAssits = dialogueIndex

View File

@@ -54,8 +54,6 @@
</div>
<div class="right_content_block_box">
<div class="right_content_block" ref="rightContentBlockBox" @mouseenter="mouseenter($event,'like')">
<div class="right_content_body">
<div class="right_content_header">
<div class="content_header_left">
@@ -63,19 +61,23 @@
{{ $t('HomeView.SelectedDesign') }}
</span>
</div>
<div class="generalModel_state">
<!-- <div class="generalModel_state_item smail" style="margin-right: 0;">
<div class="generalModel_state" v-show="domHidden || !recycleDomHidden">
<div v-show="isUnfold" class="generalModel_state_item smail" style="margin-right: 1rem;">
<span>Type :</span>
<a-select
v-model:value="widthValue.value"
v-model:value="resultType"
size="large"
style="width: 15rem"
placeholder="Please select"
:options="widthList"
@change="upDataLikeListType(500)"
:options="resultTypeList"
@change="changeResultType"
>
</a-select>
</div> -->
</div>
<div @click="setUnfold" class="generalModel_state_item smail" style="margin-right: 1rem;">
<span>Unfold :</span>
<a-switch v-model:checked="isUnfold" @change="setUnfold" />
</div>
<div class="generalModel_state_item smail" style="margin-right: 0;">
<span>Size :</span>
<a-select
@@ -92,15 +94,19 @@
</div>
<div class="right_content_img_block scroll_style active">
<div class="right_content_img_item" ref="likeItemDom" designType="like" :index="0">
<!-- likeDesignCollectionList -->
<div class="content_img_block content_img_GetWidth active" :style="likeStyle"
v-for="(design, index) in likeDesignCollectionList" :key="design.id || design.designItemId">
v-for="(design, index) in selectLikeDesign" :key="design.id || design.designItemId">
<div class="content_img_flex"
@mousedown.stop="designMousedown(getMousePosition($event,false),design.userLikeSortId || design.id,'like')"
@touchstart.passive="designMousedown(getMousePosition($event,true),design.userLikeSortId || design.id,'like')"
:class="[design.resultType]"
@mouseenter="startHover(getMousePosition($event,false),design)"
@mouseleave="endHover(getMousePosition($event,false),design)"
@mousedown.stop="designMousedown(getMousePosition($event,false),design,'like')"
@touchstart.passive="designMousedown(getMousePosition($event,true),design,'like')"
@click="designDetail(
design,
index,
likeDesignCollectionList,
selectLikeDesign,
'like'
)">
<img class="content_img"
@@ -111,33 +117,36 @@
<img class="content_img" v-show="!design?.generateAwait" :src="design.designOutfitUrl||design.url"
:key="design.designOutfitUrl" designType="like" :index="index"/>
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
</div>
<div class="btn" v-show="!design?.generateAwait">
<i @click.stop="openEditBtn(design.designItemId||design.id)" class="fi fi-br-menu-dots-vertical"></i>
<div class="btnOpen" @click.stop="()=>openEditBtnId=-1" :class="{active:openEditBtnId == design.designItemId||openEditBtnId == design.id}">
<div class="item" @click="dislikeDesignCollection(design,design.resultType,index)">
<div class="text">On Like</div>
<div class="icon iconfont icon-jushoucanggift icon_like">
<!-- <div class="content_img_block_child" @click.stop>
<div class="content_img_block content_img_GetWidth active childItem"
:style="likeStyle"
v-for="childItem,childIndex in design.childList"
>
<div class="content_img_flex">
<img class="content_img" v-show="!design?.generateAwait" :src="childItem.designOutfitUrl||childItem.url" alt="">
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
</div>
<div class="btn" v-show="!childItem?.generateAwait">
<i @click.stop="openEditBtn(childItem,childIndex,$event)" class="fi fi-br-menu-dots-vertical"></i>
</div>
</div>
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(likeDesignCollectionList,index,'PoseTransfer','add','like')">
<div class="text">Transfer Pose</div>
<div class="icon iconfont icon-jushoucanggift icon_like"></div>
<div class="content_img_block content_img_GetWidth active childItem"
:style="likeStyle"
v-for="childItem,childIndex in design.childList"
>
<div class="content_img_flex">
<img class="content_img" v-show="!design?.generateAwait" :src="childItem.designOutfitUrl||childItem.url" alt="">
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
</div>
<div class="btn" v-show="!childItem?.generateAwait">
<i @click.stop="openEditBtn(childItem,childIndex,$event)" class="fi fi-br-menu-dots-vertical"></i>
</div>
</div>
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(likeDesignCollectionList,index,'Relight','add','like')">
<div class="text">Relight</div>
<i class="fi fi fi-br-keyboard-brightness"></i>
</div>
<div class="item" v-show="design.resultType == 'Design'" @click="setEditDesignType(likeDesignCollectionList,index,'ToProductImage','add','like')">
<div class="text">To Product Image</div>
<i class="fi fi fi-ss-box-open"></i>
</div>
<!-- <div class="item" @click="setDeleteDesign(design,index)">
<div class="text">Delete</div>
<i class="fi fi-rr-trash icon_delete">
</i>
</div> -->
</div>
</div> -->
</div>
<div class="btn" v-show="!design?.generateAwait">
<i @click.stop="openEditBtn(design,index,$event)" class="fi fi-br-menu-dots-vertical"></i>
</div>
</div>
@@ -182,31 +191,10 @@
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
</div>
<div class="btn">
<i @click.stop="openEditBtn(design.designItemId||design.id)" class="fi fi-br-menu-dots-vertical"></i>
<div class="btnOpen" @click.stop="()=>openEditBtnId=-1" :class="{active:openEditBtnId == design.designItemId||openEditBtnId == design.id}">
<div class="item" @click="likeDesignCollection(design,index,design.resultType)">
<div class="text">Like</div>
<div class="icon iconfont icon-jushoucang icon_like">
</div>
</div>
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(designCollectionList,index,'PoseTransfer','add','disLike')">
<div class="text">Transfer Pose</div>
<div class="icon iconfont icon-jushoucanggift icon_like"></div>
</div>
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(designCollectionList,index,'Relight','add','disLike')">
<div class="text">Relight</div>
<i class="fi fi fi-br-keyboard-brightness"></i>
</div>
<div class="item" v-show="design.resultType == 'Design'" @click="setEditDesignType(designCollectionList,index,'ToProductImage','add','disLike')">
<div class="text">To Product Image</div>
<i class="fi fi fi-ss-box-open"></i>
</div>
<div class="item" @click="setDeleteDesign(design,index)">
<div class="text">Delete</div>
<i class="fi fi-rr-trash icon_delete">
</i>
</div>
<div class="icon iconfont icon-jushoucang icon_like" @click="likeDesignCollection(design,index,design.resultType)">
</div>
<i class="fi fi-rr-trash icon_delete" @click="setDeleteDesign(design,index)">
</i>
</div>
</div>
@@ -219,11 +207,29 @@
<span class="icon iconfont icon-xiala" :class="{'active':recycleDomHidden}"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div ref="designBtn" class="btnOpen" @click.stop="()=>openEditBtnId=-1" :class="{active:openEditBtnId == selectEditBtn?.designItemId||openEditBtnId == selectEditBtn?.id}">
<div class="item" @click="dislikeDesignCollection(selectEditBtn,selectEditBtn?.resultType,selectEditBtnIndex)">
<div class="text">Delete</div>
<i class="fi fi-rr-trash icon_delete">
</i>
</div>
<div class="item" v-show="selectEditBtn?.resultType == 'ToProductImage'" @click="setEditDesignType(selectLikeDesign,selectEditBtnIndex,'PoseTransfer','add','like')">
<div class="text">Transfer Pose</div>
<i class="fi fi fi-rr-play-alt"></i>
</div>
<div class="item" v-show="selectEditBtn?.resultType == 'ToProductImage'" @click="setEditDesignType(selectLikeDesign,selectEditBtnIndex,'Relight','add','like')">
<div class="text">Relight</div>
<i class="fi fi fi-br-keyboard-brightness"></i>
</div>
<div class="item" v-show="selectEditBtn?.resultType == 'Design'" @click="setEditDesignType(selectLikeDesign,selectEditBtnIndex,'ToProductImage','add','like')">
<div class="text">To Product Image</div>
<i class="fi fi fi-ss-box-open"></i>
</div>
</div>
<collectionModal ref="collectionModal"></collectionModal>
<!-- <DesignDetail ref="designDetail" @finishRedesign="finishRedesign"></DesignDetail> -->
<!-- 导出缩略图的蒙层 start-->
@@ -249,7 +255,7 @@
</template>
<script lang="ts">
import { defineComponent, h, ref, computed, toRefs, inject,provide,nextTick,reactive,onBeforeUnmount, toRef} from "vue";
import { defineComponent, h, ref, computed, toRefs, inject,provide,nextTick,reactive,onBeforeUnmount, toRef, watch} from "vue";
// import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import NewCollectionReview from "@/component/HomePage/NewCollectionReview.vue";
import generalCanvas from "@/component/modules/generalCanvas.vue";
@@ -376,9 +382,12 @@ export default defineComponent({
const dataDom = reactive({
collectionModal:null as any,
editDesignType:null as any,
designBtn:null as any,
})
const designData = reactive({
openEditBtnId:-1,
selectEditBtn:null as any,
selectEditBtnIndex:-1,
likeLoading: false, //喜欢防抖
widthList:[
{
@@ -392,6 +401,39 @@ export default defineComponent({
value:400,
}
],
selectDesign:null as any,//当前选择的design
isUnfold:false,
selectLikeDesign:[] as any,
//类型
resultTypeList:[
{
label:'All',
value:'All',
},{
label:'Design',
value:'Design',
},{
label:'Product',
value:'Product',
},
// ,{
// label:'To Product Image',
// value:'ToProductImage',
// },{
// label:'Relight',
// value:'Relight',
// },
{
label:'Pose Transfer',
value:'PoseTransfer',
},
],
resultType:'All',
oldSelectLikeDesign:null as any
})
watch(()=>likeDesignCollectionList.value.length,(val)=>{
if(val>0)uploadLikeDom()
})
provide('exportNav',exportNav)
let isShowOperate = ref(false)
@@ -476,9 +518,11 @@ export default defineComponent({
for(let i = 0;i < elArr.length;i++){
posiitonData.value.likeElList.push({
el: elArr[i],
sort: likeDesignCollectionList.value[i].sort - 1,
sort: designData.selectLikeDesign[i].sort - 1,
// sort: likeDesignCollectionList.value[i].sort - 1,
index: i,
userLikeSortId:likeDesignCollectionList.value[i].userLikeSortId || likeDesignCollectionList.value[i].id
userLikeSortId:designData.selectLikeDesign[i].userLikeSortId || designData.selectLikeDesign[i].id
// userLikeSortId:likeDesignCollectionList.value[i].userLikeSortId || likeDesignCollectionList.value[i].id
});
}
moveItem('like')
@@ -513,8 +557,10 @@ export default defineComponent({
const cancelDeleteDesign = (index:any)=>{
store.commit("cancelDeleteDesignCollectionList",index);
}
const designMousedown = (e:any,Id:number,str:string)=>{
const designMousedown = (e:any,design:any,str:string)=>{
if(design.resultType != 'Design' || designData.isUnfold)return
if(str != 'like' && showDesignMark.value) return
let Id = design.userLikeSortId || design.id
let arr = str == 'like'? posiitonData.value.likeElList:posiitonData.value.generateElList
let item:any = arr.filter((item:any)=>item.userLikeSortId == Id)[0]
item.el.style.zIndex = 2;
@@ -624,6 +670,7 @@ export default defineComponent({
}
}
const setLikeDislLike = (str:string,value:any)=>{
designData.selectLikeDesign = []
posiitonData.value.likeSelectIndex = 0
nextTick(()=>{
if(str == 'like'){
@@ -688,8 +735,24 @@ export default defineComponent({
const openCollection = (str:string)=>{
dataDom.collectionModal.init(str)
}
const openEditBtn = (id:number)=>{
designData.openEditBtnId = id
const openEditBtn = (design:any,index:number,event:any)=>{
if(designData.selectLikeDesign[index].resultType != 'Design'){
designData.selectDesign = designData.selectLikeDesign.filter((item:any)=>(item.resultType == 'Design' && item.userLikeSortId == designData.selectLikeDesign[index].parentId))[0]
}else{
designData.selectDesign = designData.selectLikeDesign[index]
}
designData.openEditBtnId = design.designItemId||design.id
designData.selectEditBtn = design
designData.selectEditBtnIndex = index
let width = event.target.offsetWidth
let height = event.target.offsetHeight
let parentRect = dataDom.designBtn.parentElement.getBoundingClientRect()
let x = event.clientX - parentRect.x - event.offsetX + width
let y = event.clientY - parentRect.y - event.offsetY + height
dataDom.designBtn.style.left = x+'px'
dataDom.designBtn.style.top = y+'px'
let removeEditBtnId = ()=>{
designData.openEditBtnId = -1
document.removeEventListener('click',removeEditBtnId)
@@ -714,7 +777,6 @@ export default defineComponent({
dataDom.editDesignType.isComparison = true
}
const addGenerateImg = (data:any)=>{
console.log(data)
if(!data.id)return
let obj = {
id:data.id,
@@ -729,6 +791,7 @@ export default defineComponent({
userGroupId:data.userGroupId || data.id,
firstFrameUrl:data.firstFrameUrl,
gifUrl:data.gifUrl,
userLikeSortId:data.userLikeSortId,
}
let list = []
if(data.listType == 'disLike'){
@@ -763,16 +826,18 @@ export default defineComponent({
list[data.isIndex].designItemId = obj.designItemId
list[data.isIndex].sourceUrl = obj.sourceUrl
}
nextTick().then(()=>{
setDesignItemStyle()
setSystemDesigner(100)
})
// nextTick().then(()=>{
// // setDesignItemStyle()
// // setSystemDesigner(100)
// uploadLikeDom()
// })
}
const setNoDesignLike = (design:any,index:any,type:any,str:string) => {
let url
let data = {
toProductImageResultId:[(design.designItemId || design.id)],
projectId: store.state.Workspace.probjects.id,
collectionSortParentId:designData?.selectDesign?.userLikeSortId || design.parentId,
} as any
let params = {} as any
if(type == 'ToProductImage' || type == 'Relight'){
@@ -788,12 +853,14 @@ export default defineComponent({
likeOrDislike:'like',
transformedId:design.id,
projectId:store.state.Workspace.probjects.id,
collectionSortParentId:designData?.selectDesign?.userLikeSortId || design.parentId,
}
}else{
params = {
likeOrDislike:'dislike',
transformedId:design.id,
projectId:store.state.Workspace.probjects.id,
collectionSortParentId:designData?.selectDesign?.userLikeSortId || design.parentId,
}
}
}else{
@@ -807,28 +874,48 @@ export default defineComponent({
Https.axiosPost(url, data,{params:params.likeOrDislike?params:''}).then(
(rv) => {
if(str == 'like'){
let value:any = {
...design,
sort:likeDesignCollectionList.value.length + 1,
userLikeGroupId:design.id,
}
design.groupDetailId = rv.groupDetailId;
store.commit(
"addLikeDesignCollectionList",
value
);
setLikeDislLike('like',value)
if(index != -1)store.commit("deleteDesignCollectionList", index);
}else{
if(index != -1)store.commit("deleteLikeDesignCollectionList", index);
store.commit("addDesignCollectionList", [design]);
likeDesignCollectionList.value.forEach((item:any)=>{
if(item.sort > design.sort){
item.sort-=1
if(item.resultType == 'Design' && item.userLikeSortId == designData.selectDesign.userLikeSortId){
item.childList.push(design)
}
})
}else{
likeDesignCollectionList.value.forEach((item:any)=>{
if(item.resultType == 'Design' && item.userLikeSortId == designData.selectDesign.userLikeSortId){
item.childList = item.childList.filter((item:any)=>{
return item.id != design.id
})
}
})
setLikeDislLike('disLike',design)
}
nextTick().then(()=>{
uploadLikeDom()
})
//一下代码是生成的design时候也可以toproduct
// if(str == 'like'){
// let value:any = {
// ...design,
// sort:likeDesignCollectionList.value.length + 1,
// userLikeGroupId:design.id,
// }
// design.groupDetailId = rv.groupDetailId;
// store.commit(
// "addLikeDesignCollectionList",
// value
// );
// setLikeDislLike('like',value)
// if(index != -1)store.commit("deleteDesignCollectionList", index);
// }else{
// if(index != -1)store.commit("deleteLikeDesignCollectionList", index);
// store.commit("addDesignCollectionList", [design]);
// likeDesignCollectionList.value.forEach((item:any)=>{
// if(item.sort > design.sort){
// item.sort-=1
// }
// })
// }
designData.likeLoading = false;
}
).catch(res=>{
@@ -898,6 +985,81 @@ export default defineComponent({
const gifPause = (e:any,item:any)=>{
e.target.src = item.firstFrame || item.firstFrameUrl//静态图片
}
const startHover = (e:any,item:any)=>{
}
const endHover = (e:any,item:any)=>{
}
const setUnfold = ()=>{
designData.isUnfold = !designData.isUnfold
if(!designData.isUnfold){
designData.selectLikeDesign = designData.oldSelectLikeDesign || []
}
uploadLikeDom()
}
const uploadLikeDom = ()=>{
if(designData.isUnfold){
// const parents = designData.selectLikeDesign.filter((item:any) => item.type === 'design');
designData.selectLikeDesign = []
likeDesignCollectionList.value.sort((a:any,b:any)=>{return a.sort - b.sort})
let childLength = 0
likeDesignCollectionList.value.forEach((likeItem:any,index:any)=>{
let item = JSON.parse(JSON.stringify(likeItem))
item.oldSort = item.sort
if(item?.childList?.length > 0){
item.childList.forEach((childItem:any,childIndex:number)=>{
childItem.sort = childIndex + item.sort + childLength
designData.selectLikeDesign.push(childItem)
})
childLength = item.childList.length + childLength
}
item.sort = childLength + item.sort
designData.selectLikeDesign.push(item)
})
designData.oldSelectLikeDesign = []
designData.resultType = 'All'
}else{
if(designData.selectLikeDesign.length == 0){
designData.selectLikeDesign = likeDesignCollectionList.value
return
}
const parents = designData.selectLikeDesign.filter((item:any) => item.resultType === 'Design');
parents.map((parent:any) => {
parent.sort = parent.oldSort||parent.sort
return {
...parent,
childList: designData.selectLikeDesign.filter((item:any) =>
item.parentId === parent.userLikeSortId && item.resultType !== 'Design'
)
};
});
designData.selectLikeDesign = parents
}
nextTick(()=>{
setSystemDesigner(0)
})
}
const changeResultType = ()=>{
if(designData.resultType != 'All'){
if(designData.oldSelectLikeDesign.length == 0)designData.oldSelectLikeDesign = designData.selectLikeDesign
if(designData.resultType == 'Product'){
designData.selectLikeDesign = designData.oldSelectLikeDesign.filter((item:any) => (item.resultType === 'ToProductImage' || item.resultType === 'Relight'));
}else{
designData.selectLikeDesign = designData.oldSelectLikeDesign.filter((item:any) => item.resultType === designData.resultType);
}
designData.selectLikeDesign.forEach((item:any,index:number) => {
item.sort = index + 1
})
}else{
designData.selectLikeDesign = designData.oldSelectLikeDesign
designData.selectLikeDesign.forEach((item:any,index:number) => {
item.sort = index + 1
})
}
nextTick(()=>{
setSystemDesigner(0)
})
}
return {
store,
...toRefs(editDesignType),
@@ -952,6 +1114,10 @@ export default defineComponent({
setGenerateAwait,
gifPlay,
gifPause,
startHover,
endHover,
setUnfold,
changeResultType,
};
},
data() {
@@ -1249,6 +1415,7 @@ export default defineComponent({
marketingSketchFiles,
moodboardPosition,
} = this.store.state.UploadFilesModule.allBoardData;
console.log(JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData)))
this.randomNum()
let modelList = [] as any;
let workspace:any = this.store.state.Workspace.probjects
@@ -1427,21 +1594,17 @@ export default defineComponent({
Https.axiosPost(Https.httpUrls.designDislike, data)
.then((rv: any) => {
if (rv) {
this.store.commit("addDesignCollectionList", [design]);
this.store.commit(
"deleteLikeDesignCollectionList",
index
);
// this.store.commit("addDesignCollectionList", [design]);
this.likeDesignCollectionList.forEach((item:any)=>{
if(item.sort > design.sort){
item.sort-=1
}
})
this.store.commit(
"deleteLikeDesignCollectionList",
index
);
this.setLikeDislLike('disLike',design)
// if (!this.likeDesignCollectionList.length) {
// this.store.commit("deleteUserGroupId");
// }
}
this.disLikeLoading = false;
})
@@ -1768,6 +1931,7 @@ export default defineComponent({
}
}
}
.designPage_body {
width: 100%;
height: 100%;
@@ -1913,6 +2077,8 @@ export default defineComponent({
.content_img_block{
width: 16rem;
height: 23rem;
position: relative;
}
}
}
@@ -2006,7 +2172,8 @@ export default defineComponent({
align-items: center;
justify-content: center;
position: absolute;
transition: top,left .3s;
transition: top,left .5s;
// transition: top,left .3s;
&:hover .btn{
display: flex;
}
@@ -2020,13 +2187,43 @@ export default defineComponent({
border: 2px solid #E0E0E0;
overflow: hidden;
background: #fff;
.content_img_block_child{
position: absolute;
border-radius: 2rem;
border: 2px solid #00000070;
background: #fff;
display: flex;
left: -43%;
transform: translateX(69%);
top: 0rem;
> .childItem{
> .content_img_flex{
border: 2px solid #00000070;
}
position: relative !important;
>img{
}
}
}
}
&.active{
.icon_like,.icon_delete{
display: block;
}
.content_img_flex {
border: 2px solid #000;
&.Design{
border: 2px solid #000;
}
&.PoseTransfer{
border: 2px solid #f0d9d0;
}
&.ToProductImage{
border: 2px solid #ccece5;
}
&.Relight{
border: 2px solid #ccece5;
}
}
}
.content_img {
@@ -2050,39 +2247,7 @@ export default defineComponent({
align-items: center;
justify-content: center;
}
> .btnOpen{
position: absolute;
width: max-content;
left: 30px;
top: 30px;
transform: translateX(-100%);
background: #e4e4e7;
padding: 12px;
border-radius: 12px;
display: none;
z-index: 99;
&.active{
display: block;
}
> .item{
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
border-radius: 8px;
cursor: pointer;
&:last-child{
margin: 0;
}
> .text{
font-size: 12px;
margin-right: 10px;
}
&:hover{
background: #efeff1;
}
}
}
.icon_like,.icon_delete {
font-size: 1.4rem;
cursor: pointer;
@@ -2106,7 +2271,7 @@ export default defineComponent({
}
}
}
.page_content_bg {
position: absolute;
width: 100%;
@@ -2130,5 +2295,38 @@ export default defineComponent({
text-align: center;
}
}
.btnOpen{
position: absolute;
width: max-content;
left: 30px;
top: 30px;
transform: translateX(-100%);
background: #e4e4e7;
padding: 12px;
border-radius: 12px;
display: none;
z-index: 999;
&.active{
display: block;
}
> .item{
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
border-radius: 8px;
cursor: pointer;
&:last-child{
margin: 0;
}
> .text{
font-size: 12px;
margin-right: 10px;
}
&:hover{
background: #efeff1;
}
}
}
}
</style>

View File

@@ -328,7 +328,8 @@ export default defineComponent({
flex-direction: column;
align-items: center;
> .top,> .model{
width: 130rem;
width: 100%;
// width: 130rem;
}
> .top{
display: flex;

View File

@@ -32,7 +32,7 @@ export default defineComponent({
components:{
design,newPorject,chat
},
emits:['setTask','newProject'],
emits:['setTask','setNewProject'],
setup(props,{emit}) {
const store = useStore();
const route = useRoute()
@@ -98,6 +98,11 @@ export default defineComponent({
store.commit("setAllBoardDataChoose",{});
store.commit("clearShowSketchboard",{});
store.commit("clearAllCollection");
store.commit("clearAllCloudList");
let arr = ['moodBoard','printBoard','sketchBoard','colorBoard']
arr.forEach((item:any)=>{
store.commit("clearAllBoardData",item);
})
}
const getWorks = (id:any)=>{
let value:any = {
@@ -214,69 +219,81 @@ export default defineComponent({
// })
// }
const getCollection = (type:string,module:any)=>{
let moduleList = [] as any
if(type == 'design'){
moduleList = ["moodBoard", "printBoard", "colorBoard", "sketchBoard",'design','mannequin']
}else{
moduleList = ["printBoard", "colorBoard", "sketchBoard",'design','toProduct','relight','poseTransfer','mannequin',"patternMaking3D",'deReconstruction']
}
if(module)moduleList = [module]
let value:any = {
"id":data.selectObject.id,
"moduleList":moduleList,
}
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
historyChooseData(rv)//设置历史数据
if(rv.mannequin.length>0){
let arr = [] as any
rv.mannequin.forEach((item:any)=>{
arr.push({
url:item.url,
id:item.mannequinRelationId,
type:item.mannequinRelationType,
collectionElementId:item.collectionId
})
})
store.commit('setProbject',{model:arr})
}
let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard']
// let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard','toProduct','relight','poseTransfer','mannequin']
if(module){
let canvasData = ['canvas','deReconstruction']
for (let index = 0; index < canvasData.length; index++) {
const item = canvasData[index];
await getCanvasData(item)
}
}
// await setitemData(allBoardData)
//还有一个canvas
if(rv.boundingBox)store.commit('setShowSketchboard',rv.boundingBox)
allBoardData.forEach((item)=>{
let value = {
type:item,
objectName:data.selectObject.type,
}
let arr = ['sketchBoard','moodBoard','printBoard','colorBoard']
if(arr.indexOf(item) != -1){
// store.dispatch('setAllBoardData',value)
}else{
store.dispatch('setModularData',value)
}
})
data.isShowMark = false
data.dataLoad = false
return new Promise((resolve, reject) => {
let moduleList = [] as any
if(type == 'design'){
nextTick(()=>{
dataDom.design.openSetData()
})
moduleList = ["moodBoard", "printBoard", "colorBoard", "sketchBoard",'design','mannequin']
}else{
moduleList = ["printBoard", "colorBoard", "sketchBoard",'design','toProduct','relight','poseTransfer','mannequin',"patternMaking3D",'deReconstruction','uploadElement']
}
}).catch(()=>{
data.isShowMark = false
data.dataLoad = false
if(module)moduleList = [module]
let value:any = {
"id":data.selectObject.id,
"moduleList":moduleList,
}
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
if(rv.uploadElement){//toproduct、relight、poseTransfer
let uploadElementData = {
str:'add',
list:rv.uploadElement,
}
store.commit('setUploadElement',uploadElementData)
}
historyChooseData(rv)//设置历史数据
if(rv.mannequin.length>0){
let arr = [] as any
rv.mannequin.forEach((item:any)=>{
arr.push({
url:item.url,
id:item.mannequinRelationId,
type:item.mannequinRelationType,
collectionElementId:item.collectionId
})
})
store.commit('setProbject',{model:arr})
}
let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard']
// let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard','toProduct','relight','poseTransfer','mannequin']
if(module){
let canvasData = ['canvas','deReconstruction']
for (let index = 0; index < canvasData.length; index++) {
const item = canvasData[index];
await getCanvasData(item)
}
}
// await setitemData(allBoardData)
//还有一个canvas
if(rv.boundingBox)store.commit('setShowSketchboard',rv.boundingBox)
allBoardData.forEach((item)=>{
let value = {
type:item,
objectName:data.selectObject.type,
}
let arr = ['sketchBoard','moodBoard','printBoard','colorBoard']
if(arr.indexOf(item) != -1){
// store.dispatch('setAllBoardData',value)
}else{
store.dispatch('setModularData',value)
}
})
data.isShowMark = false
data.dataLoad = false
if(type == 'design'){
nextTick(()=>{
dataDom.design.openSetData()
})
}
resolve('')
}).catch(()=>{
data.isShowMark = false
data.dataLoad = false
resolve('')
})
})
}
const getCanvasData = (str:any)=>{
return new Promise((resolve, reject) => {
@@ -312,7 +329,7 @@ export default defineComponent({
),
generatePrintFiles: [],
colorBoards: dealViewChooseColor(
dataValue.colorBoard
dataValue.colorBoard
),
sketchboardFiles: dealViewChooseData(
dataValue.sketchBoard,"Sketchboard"
@@ -320,7 +337,7 @@ export default defineComponent({
};
if(dataValue.moodBoard?.moodTemplateId)store.commit("setMoodTemplateId", dataValue.moodBoard.moodTemplateId);
store.commit("setAllBoardDataChoose", collectionData);
store.commit("setShowSketchboard", dataValue.sketchBoards);
if(dataValue.sketchBoards)store.commit("setShowSketchboard", dataValue.sketchBoards);
let likeDesignCollectionList:any = []
if(dataValue.design?.userLikeDetails){
dataValue.design?.userLikeDetails.map(
@@ -355,7 +372,8 @@ export default defineComponent({
}
if(dataValue.poseTransfer){
let value = {
list: dataValue.poseTransfer,
list:dataValue.poseTransfer.list,
likedList:dataValue.poseTransfer.likedList,
str:'add',
index:-1,
}
@@ -363,6 +381,7 @@ export default defineComponent({
}
if(dataValue.patternMaking3D){
let patternMaking3DData = {
collectionElementId:dataValue.patternMaking3D.collectionElementId,
threeDSimpleId:dataValue.patternMaking3D.threeDSimpleId,
url:dataValue.patternMaking3D.printMinioUrl,
printMinioUrl:getMinioUrl(dataValue.patternMaking3D.printMinioUrl),
@@ -428,16 +447,53 @@ export default defineComponent({
});
return colorList;
}
let queue = [] as any
let start = false
const chatChange = (value:any)=>{
if(value.type == 'design_control_signal' && dataDom.design){
dataDom.design.designNewCollection()
queue.push(
{
type:'design',
fun:dataDom.design.designNewCollection,
}
)
// dataDom.design.designNewCollection()
}else{
getCollection('design',value.module)
queue.push(
{
type:'promise',
fun:getCollection,
module:value.module,
str:'design'
}
)
// getCollection('design',value.module)
}
if(!start){
startQueue()
}
}
const startQueue = async ()=>{
start = true
if(queue[0].type){
if(queue[0].type == 'promise'){
await queue[0].fun(queue[0].str,queue[0].module)
}else{
await queue[0].fun()
}
queue.splice(0,1)
}
if(queue.length>0){
startQueue()
}else{
start = false
}
}
const newProject = (value:any)=>{
settingGetHistory()
router.push(`home?history=${value.id}`)
emit('setNewProject')
router.push(`home?history=${value.id}&create=true`)
data.chatData = value
}

View File

@@ -37,7 +37,15 @@
</div>
</div>
<div class="list printList" v-show="libraryOrModel == 'print'">
<selectPrint ref="selectPrint" @setPrint="setPrint"></selectPrint>
<!-- <selectPrint ref="selectPrint" @setPrint="setPrint"></selectPrint> -->
<div v-for="item in allBoardData.printboardFiles" class="item" :class="{active:item.id == patternMaking3D.collectionElementId}" @click="setSelectPrint(item)">
<img :src="item.imgUrl" alt="">
</div>
<div class="item add">
<div class="icon" @click="openSetPrint">
<i class="fi fi-br-plus"></i>
</div>
</div>
</div>
</div>
<div class="model" v-show="selectModel.id != -1">
@@ -54,7 +62,7 @@
<img v-show="!isFront" :src="selectModel.threeDLayoutList?.[0]?.url" alt="">
<img v-show="isFront" :src="selectModel.threeDLayoutList?.[1]?.url" alt="">
</div>
<threeBox v-if="imgOrThree" @saveProject="saveProject" ref="threeBox"></threeBox>
<threeBox v-if="imgOrThree" @threeSaveProject="saveProject" ref="threeBox"></threeBox>
</div>
<div class="gallery_btn" v-show="!imgOrThree" @click="setImgOrThree(true)">3D view</div>
<div class="gallery_btn" v-show="imgOrThree" @click="setImgOrThree(false)">Img view</div>
@@ -74,7 +82,7 @@
<div class="download">
<download ref="download"></download>
</div>
<scaleImage ref="scaleImage"></scaleImage>
<collection ref="collection" :isNext="false"></collection>
</div>
</template>
<script lang="ts">
@@ -87,10 +95,10 @@ import threeBox from "./three.vue"
import download from "./download.vue"
import {getMinioUrl} from '@/tool/util'
import scaleImage from "@/component/HomePage/scaleImage.vue";
import selectPrint from './selectPrint.vue'
import collection from '@/component/home/design/collection.vue'
export default defineComponent({
components:{
threeBox,download,scaleImage,selectPrint
threeBox,download,scaleImage,collection
},
props:{
},
@@ -105,8 +113,10 @@ export default defineComponent({
// return store.state.UserHabit.printType
// }),
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
patternMaking3D:computed(()=>store.state.HomeStoreModule.patternMaking3D),//选择的项目
modelList:[] as any,
sex:'Female',
allBoardData:computed(()=>store.state.UploadFilesModule.allBoardData),
sexList:computed(()=>store.state.UserHabit.sex.value),
isShowMark:false,
isNoData:false,
@@ -118,13 +128,16 @@ export default defineComponent({
maskShow:false,
libraryOrModel:'model'
})
watch(()=>data.patternMaking3D.threeDSimpleId,(newValue,oldValue)=>{
if(newValue)openSetData()
})
const sexChange = ()=>{
data.modelList = []
data.currentPage = 0,
data.isNoData = false
data.isShowMark = false
}
const createProbject:any = inject('createProbject')
const createProbject:any = inject('createProbject') as any
const setSelectModel = async (item:any)=>{
if(!data.selectObject.id)await createProbject()
data.isShowMark = true
@@ -141,7 +154,7 @@ export default defineComponent({
// dataDom.threeBox.openSetData()
// }
let stateData = {
threeDSimpleId:item.id
threeDSimpleId:item.threeDSimpleId
}
store.commit('setPatternMaking3D',stateData)
saveProject()
@@ -149,10 +162,22 @@ export default defineComponent({
data.isShowMark = false
})
}
const setSelectPrint = async (item:any)=>{
if(!data.selectObject.id)await createProbject()
let patternMaking3DData = {
collectionElementId:item.id,
url:item.imgUrl,
printMinioUrl:getMinioUrl(item.imgUrl),
};
store.commit('setPatternMaking3D',patternMaking3DData)
if(!dataDom.threeBox)dataDom.threeBox.addMaterial({url:item.imgUrl})
saveProject()
}
const dataDom = reactive({
threeBox:null as any,
download:null as any,
scaleImage:null as any,
collection:null as any,
})
const openSetData = ()=>{
nextTick(()=>{
@@ -240,16 +265,18 @@ export default defineComponent({
})
})
},2000)
}
onMounted(()=>{
openSetData()
})
const openSetPrint = ()=>{
dataDom.collection.init('printBoard')
}
return{
...toRefs(dataDom),
...toRefs(data),
sexChange,
setSelectModel,
setSelectPrint,
openSetData,
getModelList,
setImgOrThree,
@@ -257,6 +284,8 @@ export default defineComponent({
setLibraryOrModel,
openScaleImage,
setPrint,
openSetPrint,
saveProject,
}
},
directives:{
@@ -374,6 +403,46 @@ export default defineComponent({
border: 2px solid #000;
}
}
&.printList{
>.item{
width: calc(100% / 4 - 1rem);
margin:.5rem;
aspect-ratio: 1 / 1;
&.active{
opacity: .7;
transform: scale(.9);
// border: 2px solid #000;
// border-radius: 2rem;
// overflow: hidden;
}
> img{
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
&.add{
display: flex;
justify-content: center;
align-items: center;
>.icon{
border: calc(0.3rem*1.2) solid #000;
border-radius: calc(1rem*1.2);
width: 6rem;
height: 6rem;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
i{
font-weight: 600;
font-size: 2rem;
display: flex;
}
}
}
}
}
}
}
> .model ,> .flatPatterm{

View File

@@ -1,700 +0,0 @@
<template>
<div class="printboard_upload_modal">
<div class="printboard_left_upload">
<div class="left_upload_header">
<div class="upload_header_item">
<div class="switch_type_list">
<div
class="switch_type_item"
:class="[openClick == 1 ? 'select_swtich' : '']"
>
<span @click.stop="open(1)">{{ $t('PrintboardUpload.Upload') }}</span>
</div>
<div
class="switch_type_item"
:class="[openClick == 2 ? 'select_swtich' : '']"
>
<span @click.stop="open(2)">{{ $t('PrintboardUpload.Library') }}</span>
</div>
<div
class="switch_type_item Guide_1_2_1"
:class="[openClick == 3 ? 'select_swtich' : '']"
>
<span @click.stop="open(3)">{{ $t('PrintboardUpload.Generate') }}</span>
</div>
<div v-show="openClick == 3" class="generalMenu_printModel printMenu">
<div @click.stop="openPrintModel"><span>{{ scene.name }}<i class="icon iconfont icon-xiala" :class="{forbidden:openMenu}"></i></span> </div>
<ul v-show="openMenu">
<li
v-for="item,index in printCatecoryList"
class="printModel_item"
@click.stop="setSceneList(item)"
:title="item.value == 'Pattern'?$t('PrintboardUpload.PatternTitle'):
item.value == 'Logo'?$t('PrintboardUpload.LogoTitle'):
item.value == 'Slogan'?$t('PrintboardUpload.SloganTitle'):''"
>{{ item.name }}</li>
</ul>
</div>
</div>
</div>
</div>
<div v-show="openClick == 1" class="printboard_body">
<div class="upload_img_body">
<div class="upload_item">
<div :class="['upload_file_item']" v-for="(file, index) in fileList" :key="file">
<div class="upload_file_img_block">
<div class="upload_file_item_content" v-show="file?.status === 'uploading'">
<a-spin :indicator="indicator" tip="Uploading..."/>
</div>
<div class="upload_file_item_content" v-show="file?.status === 'done'" @click="setSetchboardGenerate(file.resData)">
<img v-lazy="file.imgUrl" class="upload_img" :key="file.imgUrl">
<sketchCategory :disignTypeList="printCatecoryList" :generateList="fileList" :isSetSketchCategory="true" :item="file" ></sketchCategory>
<div class="delete_like_file_block" @click.stop="deleteFile(file)">
<span
class="icon iconfont icon-shanchu"
></span>
</div>
</div>
</div>
<!-- <div class="pin_block" v-show="file?.status === 'done'">
<a-checkbox v-model:checked="file.pin">{{ $t('PrintboardUpload.PIN') }}</a-checkbox>
</div> -->
</div>
<div class="upload_file_item upload_component" v-show="printboardList.length < 16">
<a-upload
:action="uploadUrl + '/api/element/upload'"
:capture="null"
list-type="picture-card"
:before-upload="beforeUpload"
:data="{
...upload
}"
:headers="{Authorization:token}"
v-model:file-list="fileList"
:customRequest="function(){}"
:maxCount="16 - printboardList.length+fileList.length"
accept=".jpg,.png,.jpeg,.bmp"
@change="fileUploadChange"
>
<div
class="upload_tip_block"
v-show="printboardList.length < 16"
>
<i class="fi fi-br-upload"></i>
<!-- <img class="upload_img_icon" src="@/assets/images/homePage/add_file.png"> -->
</div>
</a-upload>
</div>
</div>
</div>
</div>
<Material v-show="openClick == 2"
ref="Material"
msg="Printboard"
@setLibrary = setSetchboardGenerate
@confirmSelect="confirmSelect"
:disignTypeList="printCatecoryList"
></Material>
<Generate v-show="openClick == 3" ref="Generate" @setLibrary = setSetchboardGenerate :scene="scene" :sketchCatecoryList="printCatecoryList" msg="Printboard"></Generate>
</div>
<!-- 取消请求 -->
<scaleImage ref="scaleImage"></scaleImage>
<Cropper ref="Cropper" @handleCropperSuccess="handleCropperSuccess" @closeCropper="deletUploadFile()" :cropperFileData="cropperFileData" :isUpload="isUpload"></Cropper>
</div>
</template>
<script lang="ts">
import { defineComponent,h,ref,computed,inject,createVNode,provide, nextTick } from 'vue'
import { LoadingOutlined } from '@ant-design/icons-vue';
import {getCookie} from '@/tool/cookie'
import {getUploadUrl} from '@/tool/util'
import {useStore} from 'vuex'
import { Https } from "@/tool/https";
import { Modal,message,Upload} from 'ant-design-vue';
import Cropper from '@/component/HomePage/Cropper.vue'
import Material from '@/component/HomePage/Material.vue'
import Generate from "@/component/HomePage/Generate.vue";
import GO from "@/tool/GO";
import { useI18n } from 'vue-i18n';
import { openGuide,driverObj__ } from "@/tool/guide";
import scaleImage from "@/component/HomePage/scaleImage.vue";
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import sketchCategory from "@/component/HomePage/sketchCategory.vue";
import axios from 'axios'
import generalMenu from "@/component/HomePage/generalMenu.vue";
import { overflow } from 'html2canvas/dist/types/css/property-descriptors/overflow';
export default defineComponent({
components:{
Cropper,
Material,
Generate,
scaleImage,
generalMenu,
sketchCategory,
},
emits: ['setPrint'],
setup(){
let store:any =useStore()
let fileList:any = ref([]),//选中的文件id数据
printImgList:any = ref([]), //print的印花图片
moodBoards:any = computed(()=>{return store.state.UploadFilesModule.moodboard})
let openClick: any = ref(1);
let {t} = useI18n()
let isTest = ref()
let userInfo:any = {}
let workspace:any = ref({})
let scene = ref({
name:'Pattern',
value:'Pattern'
})
let openMenu = ref(false)
let printCatecoryList:any = computed(()=>{
return store.state.UserHabit.printType
})
return {
fileList,
printImgList,
moodBoards,
openClick,
t,
isTest,
userInfo,
workspace,
scene,
openMenu,
printCatecoryList,
}
},
computed:{
},
watch:{
},
data(){
return {
swtich_type:'upload',
indicator : h(LoadingOutlined, {
style: {
fontSize: '2.4rem*1.2)',
},
spin: true,
}),
upload:{
isPin:0,
gender:'',
level1Type:'Printboard',
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
},
token:'',
uploadUrl:'',
store:useStore(),
cropperFileData:{name:'',uid:''}, //裁剪的原始文件数据
currentFileNum:0, //当前上传的文件数
isUpload:false,
printboardList:computed(()=>{
return useStore().state.UploadFilesModule.printboard
}),
isTextarea:false,
isInputFocus:false,
isUseGenerate:true
}
},
mounted(){
let userInfo:any = getCookie("userInfo")
this.userInfo = JSON.parse(userInfo);
this.token = getCookie('token') || ''
let isTest:any = getCookie('isTest')
this.isTest =JSON.parse(isTest)
this.uploadUrl = getUploadUrl()
this.workspace = computed(()=>{
return this.store?.state?.Workspace?.workspace
})
},
methods:{
setSetchboardGenerate(item:any){
this.$emit('setPrint',item)
},
open(num: Number) {
this.openClick = num;
let material:any = this.$refs.Material
if(num == 2){
material.init('Printboard')
}
if(num == 3){
this.scene = this.printCatecoryList[0]
}
},
fileUploadChange(data:any){
let file = data.file
// let res = JSON.parse(data?.xhr?.response);
file.id_ = GO.id++
file.type_ = {
type1:'upload',
type2:'Printboard'
};
file.pin = false;
// file.id = res.data.id?res.data.id:""
let Cropper:any = this.$refs.Cropper
if(this.currentFileNum === 1){
var reader = new FileReader();
reader.onload = (e:any) => {
let data_new;
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
data_new = window.URL.createObjectURL(new Blob([e.target.result]));
} else {
data_new = e.target.result;
}
Cropper.getOptionImg(data_new)
};
// 转化为base64
// reader.readAsDataURL(file)
// 转化为blob
reader.readAsArrayBuffer(file.originFileObj);
this.cropperFileData = file
Cropper.changeShowModal(true)
}else{
this.customRequest(file)
}
},
beforeUpload(file:any,fileList:any){
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg' || file.type === 'image/bmp';
if (!isJpgOrPng) {
message.info(this.t('PrintboardUpload.jsContent1'));
}
const isLt2M = file.size / 1024 / 1024 < 5;
if (!isLt2M) {
message.info(this.t('PrintboardUpload.jsContent2'));
}
if(isJpgOrPng && isLt2M){
this.currentFileNum = fileList.length
}else{
return (isJpgOrPng && isLt2M) || Upload.LIST_IGNORE;
}
},
deleteFile(item:any){
// this.fileList.splice(item, 1)
// this.store.commit('setPrintboardFile',this.fileList)
if(item.type_.type1 == 'generate' || item.type_.type1 == 'material'){
item.jsContent1 = this.t('uploadFile.jsContent1',{maxImg:16})
this.store.commit("addGenerateMaterialFils", item);
}else{
this.fileList = this.store.state.UploadFilesModule.printboardFiles
let moodboard
this.store.state.UploadFilesModule.printboardFiles.forEach((items:any,index:Number)=>{
if(items.id_ == item.id_){
moodboard = index
}
})
this.fileList.splice(moodboard,1)
this.store.commit("setPrintboardFile", this.fileList);
}
},
likeFile(item:any,str:string){
if(str == 'like'){
let data = {
generateDetailId:item.id,
level1Type:"Printboard",
level2Type: item.categoryValue,
gender:this.workspace.sexEnum.value,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
Https.axiosPost(Https.httpUrls.generateLike, data).then(
(rv) => {
item.like = true
}
).catch(res=>{
});
}else{
let data = {
generateDetailId:item.id,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
Https.axiosGet(Https.httpUrls.generateDislike, {params:data}).then(
(rv) => {
item.like = false
}
).catch(res=>{
});
}
},
customRequest(data:any){
let new_data = {
...this.upload,
file:data.originFileObj
}
let fileUid = data.uid
Https.axiosPost('/api/element/upload', new_data,{headers:{'Content-Type': 'multipart/form-data'}}).then(
(rv: any) => {
if (rv) {
for(let file of this.fileList){
if(fileUid === file.uid){
file.status = 'done'
file.imgUrl = rv.url
file.pin = false
file.id = rv.id
file.category = this.printCatecoryList[0].name
file.categoryValue = this.printCatecoryList[0].value
file.resData = rv
}
}
let fileList = this.fileList.filter((v:any)=> v.status === 'done')
this.store.commit('setPrintboardFile',fileList)
}
}
).catch((res)=>{
let index = -1
this.fileList.forEach((ele:any,index1:any) => {
if(fileUid === ele.uid){
index = index1
}
});
if(index > -1){
this.fileList.splice(index, 1)
}
});
},
closeCropper(type:any){
if(this.isUpload){
return
}
if(type == 'error'){
let index = -1
this.fileList.forEach((ele:any,index1:any) => {
if(this.cropperFileData.uid === ele.uid){
index = index1
}
});
if(index > -1){
this.fileList.splice(index, 1)
}
}
let Cropper:any = this.$refs.Cropper
Cropper.closeCropper()
},
deletUploadFile(){
let index = -1
this.fileList.forEach((ele:any,index1:any) => {
if(this.cropperFileData.uid === ele.uid){
index = index1
}
});
if(index > -1){
this.fileList.splice(index, 1)
}
},
handleCropperSuccess(event:any){
let {file, fileData} =event
let new_data = {
...this.upload,
file:file
}
if(this.isUpload){
return
}
this.isUpload = true
const hide = message.loading('loading', 0);
Https.axiosPost('/api/element/upload', new_data,{headers:{'Content-Type': 'multipart/form-data'}}).then(
(rv: any) => {
for(let file of this.fileList){
if(fileData.uid === file.uid){
file.status = 'done'
file.imgUrl = rv.url
file.id = rv.id
file.resData = rv
file.category = this.printCatecoryList[0].name
file.categoryValue = this.printCatecoryList[0].value
}
}
let fileList = this.fileList.filter((v:any)=> v.status === 'done')
this.isUpload = false
this.closeCropper('success')
this.cropperFileData = {name:'',uid:''}
// this.store.commit('setPrintboardFile',fileList)
hide()
}
).catch(res=>{
let index = -1
this.fileList.forEach((ele:any,index1:any) => {
if(fileData.uid === ele.uid){
index = index1
}
});
if(index > -1){
this.fileList.splice(index, 1)
}
this.cropperFileData = {name:'',uid:''}
this.isUpload = false
this.closeCropper('error')
hide()
});
},
recollection(){
let arr = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles))
let setboard = {
generate:[] as any,
material:[] as any,
moodboard:[] as any,
}
arr.forEach((v:any)=>{
v.pin = v.pin == 1?true:false
this.printCatecoryList.forEach((item:any) => {
if(v.level2Type == item.value){
v.category=item.name
v.categoryValue=item.value
}
});
if(v.type_.type1 == 'generate'){
setboard.generate.push(v)
}else if(v.type_.type1 == 'material'){
setboard.material.push(v)
}else{
setboard.moodboard.push(v)
}
})
this.store.commit("setPrintboardGenerateFiles", setboard.generate);
this.store.commit("setPrintboardMaterialFiles", setboard.material);
this.store.commit("setPrintboardFile", setboard.moodboard);
this.fileList = setboard.moodboard
// this.printImgList = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.generatePrintFiles))
this.store.commit('setPrintboardFile',this.fileList)
// this.store.commit('setGeneratePrintFile',this.printImgList)
},
confirmSelect(event:any){
for(let item of event){
let data = {
imgUrl:item.url,
resData:item,
pin:false,
id:item.id,
status:'done',
}
if(this.fileList.length == 15){
message.warning(this.t('PrintboardUpload.jsContent3'))
break
}
this.fileList.push(data)
}
this.store.commit('setPrintboardFile',this.fileList)
},
scaleImage(index:any){
let scaleImage:any = this.$refs.scaleImage
scaleImage.isLike = false
scaleImage.init(this.printboardList,index)
},
setSceneList(data:any){
if(this.scene.value === data.value) return
this.scene = data
let generate:any = this.$refs.Generate
generate.sketchboardList = []
generate.searchPictureName = ''
this.openMenu = false
},
openPrintModel(){
if(this.openMenu)return
document.addEventListener('click',this.removePrintModel)
this.openMenu = true
},
removePrintModel(){
this.openMenu = false
document.removeEventListener('click',this.removePrintModel)
}
}
})
</script>
<style lang="less" scoped>
.printboard_upload_modal{
// padding: calc(1rem*1.2) calc(1rem*1.2) calc(1.8rem*1.2) calc(1rem*1.2);
height: 100%;
display: flex;
.printboard_left_upload{
display: flex;
flex-direction: column;
width: 100%;
.switch_type_list {
display: flex;
align-items: center;
position: relative;
z-index: 11;
.switch_type_item {
display: flex;
align-items: center;
// padding: 0 2rem*1.2);
height: calc(4rem*1.2);
border-radius: calc(0.8rem*1.2);
line-height: calc(4rem*1.2);
font-size: var(--aida-fsize1-8);
// margin-right: 2.2rem*1.2);
margin-right: calc(8rem*1.2);
color: #000;
cursor: pointer;
position: relative;
text-align: center;
transform-origin: left;
transform: scale(1);
transition: 0.3s all;
&.switch_type_item::before {
position: absolute;
content: "";
display: block;
background: #000;
height: calc(.3rem*1.2);
left: 50%;
transform: translateX(-50%);
bottom: calc(.6rem*1.2);
width: 0px;
transition: 0.3s all;
}
&.select_swtich {
color: #000;
// font-weight: 900;
transform: scale(1.15);
}
&.select_swtich::before {
width: 100%;
}
.switch_icon {
font-size: calc(1.8rem*1.2);
margin-right: calc(0.8rem*1.2);
}
}
.printMenu{
margin-right: 0;
margin-top: auto;
position: relative;
margin-left: 2rem;
>div{
width: 14rem;
font-size: var(--aida-fsize1-6);
border: 0;
i{
transition: all .3s;
display: inline-block;
margin-left: 1rem;
}
.forbidden{
transform: rotate(180deg);
}
}
ul{
width: 14rem;
}
}
.switch_type_item:nth-child(3){
margin: 0;
}
}
.printboard_body{
display: flex;
flex-direction: column;
// height: calc(100% - 5rem*1.2));
flex: 1;
padding-top: calc(2.5rem*1.2);
height: calc(30rem*1.2);
overflow-x: hidden;
&.printboard_body::-webkit-scrollbar {
display: none;
}
.upload_img_body{
height: calc(100% - 3rem*1.2);
overflow-y: auto;
margin-bottom: calc(1rem*1.2);
&.upload_img_body::-webkit-scrollbar {
display: none;
}
}
.upload_file_item{
margin: 0 calc(2rem*1.2) calc(2rem*1.2) 0;
// margin: 0 2rem*1.2) 2rem*1.2) 0;
display: inline-block;
width: 10rem;
height: 10rem;
border: 1px solid #f5f5f5;
vertical-align: top;
position: relative;
text-align: center;
img{
width: auto;
object-fit: cover;
height: 100%;
max-width: 100%;
}
.upload_file_img_block{
height: 100%;
}
&.upload_component{
}
.checked{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
display: none;
justify-content: center;
align-items: center;
}
&.modal_img_item:hover .checked{
display: flex;
}
.upload_file_item_content{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: relative;
cursor: pointer;
.upload_img{
display: block;
max-height: 100%;
width: auto;
max-width: 100%;
object-fit: cover;
}
&:hover .delete_like_file_block{
display: block;
}
}
}
}
}
}
</style>

View File

@@ -63,7 +63,7 @@ export default defineComponent({
},
props:{
},
emits:['saveProject'],
emits:['threeSaveProject'],
setup(props,{emit}) {
const store = useStore();
const data = reactive({
@@ -265,8 +265,6 @@ export default defineComponent({
texture.anisotropy = 32; // 提高纹理清晰度
data.group?.traverse((child:any) => {
if (child.isMesh) {
// console.log(child.name)
// 5. 创建新材质(根据需求选择材质类型)
const textureWidth = texture.image.width;
const textureHeight = texture.image.height;
const box = new THREE.Box3().setFromObject(child);
@@ -278,9 +276,6 @@ export default defineComponent({
if(patternMaking3D.x)data.repeat.x = patternMaking3D.x
if(patternMaking3D.y)data.repeat.y = patternMaking3D.y
// texture.repeat.set(1, 1); // 纹理重复次数
texture.repeat.set(2 - data.repeat.x, 2 - data.repeat.y); // 纹理重复次数
const newMaterial = new THREE.MeshStandardMaterial({
map: texture, // 基础颜色贴图
roughness: 0.7, // 表面粗糙度 (0-1)
@@ -338,7 +333,6 @@ export default defineComponent({
y:data.repeat.y,
}
store.commit('setPatternMaking3D',value)
})
}
const addModel = async (url:any)=>{
@@ -421,23 +415,23 @@ export default defineComponent({
await setModel(modeUrl)
let patternMaking3D = store.state.HomeStoreModule.patternMaking3D
if(patternMaking3D.url)await addMaterial({url:patternMaking3D.url})
changeRepeat(null)
data.load.state = false
}
const changeRepeat = (e:any)=>{
if(data.isLock)data.repeat.x = e
if(data.isLock)data.repeat.y = e
if(data.isLock && e)data.repeat.x = e
if(data.isLock && e)data.repeat.y = e
clearTimeout(dataTime.updataRepeat)
dataTime.updataRepeat = setTimeout(()=>{
data.repeat.x = data.repeat.x == 6 ? 5.999 : data.repeat.x
data.repeat.y = data.repeat.y == 6 ? 5.999 : data.repeat.y
data.textureLoader.repeat.set(6 - data.repeat.x,6 - data.repeat.y); // 纹理重复次数
let value = {
x:data.repeat.x,
y:data.repeat.y,
}
store.commit('setPatternMaking3D',value)
emit('saveProject')
emit('threeSaveProject')
},1000)
}
const setLock = ()=>{

View File

@@ -10,13 +10,23 @@
<div class="text">Choose design to transfer</div>
</div>
<div class="imgBox" v-mousewheel>
<div class="item" :class="{active:item.id == selectImg.id}" v-for="item in currentList" @click="selectImgItem(item)">
<img :src="item.imgUrl || item.url" alt="">
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
</div>
<div class="item" :class="{active:item.id == selectImg.id}" v-for="item in fileList" @click="selectImgItem(item)">
<!-- <div class="item" :class="{active:item.id == selectImg.id}" v-for="item in currentList" @click="selectImgItem(item)">
<img :src="item.imgUrl || item.url" alt="">
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
</div> -->
<div class="item" :class="{active:item.id == selectImg.id}" v-for="item,index in fileList" @click="selectImgItem(item)">
<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="">
<div v-show="item.status != 'uploading'" class="btnBox">
<div :class="{active:item.isChecked}">
<i class="fi fi-br-check"></i>
</div>
<div @click.stop="setUploadDelete(item,index)">
<i class="fi fi-rr-trash icon_delete"></i>
</div>
</div>
</div>
<div class="upload_item item">
<div class="upload_file_item">
@@ -43,7 +53,6 @@
</a-upload>
</div>
</div>
</div>
</div>
<div class="poses">
@@ -107,7 +116,7 @@
</div>
</template>
<script lang="ts">
import { defineComponent,computed,ref,inject,nextTick,createVNode,toRefs, reactive, onMounted} from 'vue'
import { defineComponent,computed,ref,inject,nextTick,watch,createVNode,toRefs, reactive, onMounted} from 'vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { Https } from "@/tool/https";
import { useStore } from "vuex";
@@ -144,8 +153,8 @@ export default defineComponent({
projectId:computed(()=>store.state.Workspace.probjects.id)
},
waitList:[],
likeList:computed(()=>store.state.HomeStoreModule.poseTransfer),
noLikeList:[ ],
likeList:computed(()=>store.state.HomeStoreModule.poseTransfer.likedList),
noLikeList:computed(()=>store.state.HomeStoreModule.poseTransfer.list),
isGenerate:false,//判断是否正在进行generate
remGenerate:false,
removeGenerate:false,
@@ -153,6 +162,16 @@ export default defineComponent({
poseList:[],
selectPose:null as any,
})
watch(()=>store.state.HomeStoreModule.uploadElement.length,(newVal,oldVal)=>{
data.fileList = store.state.HomeStoreModule.uploadElement
})
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:[
{
@@ -180,6 +199,10 @@ export default defineComponent({
scaleVideo:null as any,
})
const selectImgItem = (item:any,)=>{
if(item.isChecked){
item.isChecked = !item.isChecked
return
}
data.selectImg = item
data.fileList.forEach((listItem:any)=>listItem.isChecked = false)
data.currentList.forEach((listItem:any)=>listItem.isChecked = false)
@@ -194,18 +217,17 @@ export default defineComponent({
if(data.poseList.length == 0){
getPoseList()
}
setCloudImg()
}
const setCloudImg = ()=>{
let arr = store.state.UploadFilesModule.cloudList
let list = JSON.parse(JSON.stringify(arr.poseTransfer)) || []
list.forEach((item:any)=>{
item.url = item.firstFrameUrl
data.noLikeList.unshift(item)
})
store.commit('clearCloudList','poseTransfer')
// setCloudImg()
}
// const setCloudImg = ()=>{
// let arr = store.state.UploadFilesModule.cloudList
// let list = JSON.parse(JSON.stringify(arr.poseTransfer)) || []
// list.forEach((item:any)=>{
// item.url = item.firstFrameUrl
// data.noLikeList.unshift(item)
// })
// store.commit('clearCloudList','poseTransfer')
// }
const gifPlay = (e:any,item:any)=>{
e.target.src = item.gif//使用gif图片
}
@@ -234,9 +256,8 @@ export default defineComponent({
if(data.isGenerate)return
data.isGenerate = true
data.remGenerateTime = setTimeout(()=>{
data.remGenerate = true
},10000)
// data.remGenerateTime = setTimeout(()=>{
// },10000)
let value = {
poseId:data.selectPose,
projectId:store.state.Workspace.probjects.id,
@@ -244,6 +265,7 @@ export default defineComponent({
modelName:speed.speedData.value,
}
Https.axiosPost(Https.httpUrls.poseTransform,value).then((rv)=>{
data.remGenerate = true
data.noLikeList.unshift({taskId:rv})
setGenerate(rv)
}).catch((res:any)=>{
@@ -254,6 +276,7 @@ export default defineComponent({
}
const setGenerate = (dataList:any)=>{
let list:any = dataList
data.waitList = list
let dataNum = dataList.length
let state = true
data.generateTime = setInterval(()=>{
@@ -271,30 +294,24 @@ export default defineComponent({
element.url = element.firstFrameUrl
let index = data.noLikeList.findIndex((obj:any) => obj.taskId === element.taskId);
data.noLikeList[index] = element
console.log(list)
list = ''
// if(list?.filter)list = list?.filter((item:any) => item !== element.taskId);
store.dispatch('getCredits')
}else if(element.status == 'Fail'){
let index = data.noLikeList.findIndex((obj:any) => obj.taskId === element.taskId);
data.noLikeList.splice(index,1)
clearInterval(data.generateTime)
clearInterval(data.remGenerateTime)
data.remGenerate = false
data.isGenerate = false
store.dispatch('getCredits')
}
});
data.waitList = list
if((!list)|| (rv.filter((item:any)=>item.status == 'Invalid').length ==list.length)){
if(rv.filter((item:any)=>item.status == 'Invalid').length ==dataNum){
message.info(t('Generate.effectPoor'));
}else{
}
// this.store.dispatch('getCredits')
clearInterval(data.generateTime)
clearInterval(data.remGenerateTime)
data.remGenerate = false
data.isGenerate = false
}
}
}
).catch(res=>{
data.noLikeList = data.noLikeList.filter((item:any)=>item.taskId != list)
let index = data.noLikeList.findIndex((obj:any) => obj.taskId === list);
data.noLikeList.splice(index,1)
clearInterval(data.generateTime)
clearInterval(data.remGenerateTime)
data.isGenerate = false
@@ -417,7 +434,7 @@ export default defineComponent({
(rv) => {
if(str == 'like'){
let value = {
list:[item],
likedList:[item],
str:'add',
index:-1,
}
@@ -425,7 +442,7 @@ export default defineComponent({
data.noLikeList.splice(index,1)
}else{
let value = {
list:[item],
likedList:[item],
str:'splice',
index:index,
}
@@ -455,6 +472,19 @@ export default defineComponent({
}
const setSpeed = (item:any)=>{
speed.speedData = item
speed.speedState = false
}
const setUploadDelete = (item:any,index:any)=>{
let value = {
id:item.id
}
Https.axiosPost(Https.httpUrls.toProductImageElementDelete,{},{params:value}).then((rv)=>{
let storeData = {
str:'delete',
index,
}
store.commit('setUploadElement',storeData)
})
}
return{
...toRefs(speed),
@@ -476,6 +506,7 @@ export default defineComponent({
selectPose,
openSpeed,
setSpeed,
setUploadDelete,
}
},
directives:{
@@ -512,6 +543,7 @@ export default defineComponent({
flex-direction: column;
align-items: center;
padding-right: 3.8rem;
width: 39rem;
&.active{
width: 0;
overflow: hidden;
@@ -562,14 +594,13 @@ export default defineComponent({
}
}
> .configuratioBox > .configuratio{
width: 31.8rem;
width: 100%;
padding-bottom: 1rem;
> .title{
// font-size: 2.6rem;
}
> .content{
// margin-top: 4rem;
margin-top: 1.8rem;
> .selectImg,> .poses{
display: flex;
flex-direction: column;
@@ -602,6 +633,40 @@ export default defineComponent({
top: 2rem;
right: 2rem;
}
> .btnBox{
position: absolute;
right: 1rem;
top: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
> div{
width: 2rem;
height: 2rem;
display: flex;
border: 1px solid;
align-items: center;
border-radius: .5rem;
justify-content: center;
cursor: pointer;
margin-bottom: 1rem;
> i{
display: flex;
}
>.fi-br-check{
color: #fff;
display: none;
}
&.active{
background: #000;
> i{
display: flex;
color: #fff;
}
}
}
}
> img{
width: 100%;
object-fit: contain;
@@ -627,8 +692,6 @@ export default defineComponent({
}
}
> .likeBox ,> .noLikeBox{
margin-top: 1.8rem;
flex: 1;
position: relative;
> .element{

View File

@@ -10,56 +10,57 @@
<!-- <div class="productImg_content_item_title productImg_content_item_title_menu">
<generalMenu class="productImg_content_item_title_menubtn" :class="{hideEvents:driver__.driver}" :dataList="productimgMenuList" @setprintModel="setproduct" :item="productimgMenu"></generalMenu>
</div> -->
<div class="productImg_content_item_title">{{$t('ProductImg.SelectCollection')}}</div>
<div class="productImg_content_item_imgBox generalScroll upload_item" v-mousewheel>
<div class="content_item_imgBox_itemImg" v-for="item,index in selectList[productimgMenu.value]" :key="item.id" >
<img @click="setGenerate(item)" v-lazy="item.designOutfitUrl?item.designOutfitUrl:item.url" alt="" :class="[item?.isChecked?'active':'']">
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
<div class="selectImg">
<div class="head">
<div class="text">{{$t('ProductImg.SelectCollection')}}</div>
</div>
<div class="content_item_imgBox_itemImg" v-for="(file, index) in fileList[productimgMenu.value]" :key="file">
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" >
<a-spin
:indicator="indicator"
tip="Uploading..."
/>
</div>
<div
class="imgBox"
v-show="file?.status === 'done'"
>
<img @click="setGenerate(file)" :class="[file?.isChecked?'active':'']" :src="file?.imgUrl" class="upload_img"/>
<a-checkbox v-model:checked="file.isChecked"></a-checkbox>
</div>
</div>
<div class="upload_file_item upload_component">
<!-- :action="uploadUrl + '/api/history/toProductImageElementUpload'" -->
<a-upload
:action="uploadUrl + '/api/history/toProductImageElementUpload'"
list-type="picture-card"
:capture="null"
:data="{
...upload,
}"
:headers="{ Authorization: token }"
:before-upload="beforeUpload"
v-model:file-list="fileList[productimgMenu.value]"
:multiple="true"
:maxCount="8"
accept=".jpg,.png,.jpeg,.bmp"
@change="(file) => fileUploadChange(file)"
>
<div
class="upload_tip_block"
>
<i class="fi fi-br-upload"></i>
<!-- <img class="upload_img_icon" src="@/assets/images/homePage/add_file.png"> -->
<div class="imgBox" v-mousewheel>
<!-- <div class="item" :class="[item?.isChecked?'active':'']" v-for="item in selectList[productimgMenu.value]" @click="setGenerate(item)">
<img :src="item.imgUrl || item.url" alt="">
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
</div> -->
<div class="item" :class="[item?.isChecked?'active':'']" v-for="item,index in fileList[productimgMenu.value]" @click="setGenerate(item)">
<div class="" v-if="item.status == 'uploading'" style="display: flex;align-items: center;">
<a-spin size="large" />
</div>
</a-upload>
<img v-show="item.status != 'uploading'" :src="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>
</div>
<div @click.stop="setUploadDelete(item,index)">
<i class="fi fi-rr-trash icon_delete"></i>
</div>
</div>
<!-- <a-checkbox v-model:checked="item.isChecked"></a-checkbox> -->
</div>
<div class="upload_item item">
<div class="upload_file_item">
<a-upload
:action="uploadUrl + '/api/history/toProductImageElementUpload'"
list-type="picture-card"
:capture="null"
:data="{
...upload,
}"
:headers="{ Authorization: token }"
:before-upload="beforeUpload"
v-model:file-list="fileList[productimgMenu.value]"
:multiple="true"
accept=".jpg,.png,.jpeg,.bmp"
@change="(file) => fileUploadChange(file)"
>
<div
class="upload_tip_block"
>
<i class="fi fi-br-upload"></i>
<!-- <img class="upload_img_icon" src="@/assets/images/homePage/add_file.png"> -->
</div>
</a-upload>
</div>
</div>
</div>
</div>
<div v-show="productimgMenu.value == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.Similarity')}}</span>
</div>
@@ -74,15 +75,21 @@
<input type="number" readonly v-model="similarity">
</div>
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.RelightDirection')}}</span>
<span>Selection Function</span>
</div>
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_Direction">
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
</div>
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.RelightDirection')}}</span>
</div>
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
<a-select style="width: 100%;" v-model:value="RelightDirection" :options="RelightDirectionList"></a-select>
</div>
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
<span>{{$t('ProductImg.Highlight')}}</span>
</div>
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_similarity">
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
<a-slider class="system_silder"
v-model:value="brightenValue"
:tooltipVisible="false"
@@ -114,8 +121,12 @@
</div>
<div class="productImg_content_item_generate_btn input_border">
<div class="generage_btn_box ">
<div v-show="!isProductimg" class="generage_btn started_btn" @click.stop="getPrductimg">
{{ $t('Generate.Generate') }}
<div v-show="!isProductimg" class="generage_btn started_btn">
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg"></i>
<div v-show="productimgMenu.value != 'Relight'" 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>
</div>
<div v-show="isProductimg && !remProductimg" class="generage_btn started_btn" @click="getPrductimg">
<i class="fi fi-br-loading"></i>
@@ -135,32 +146,8 @@
{{ $t('ProductImg.SelectedDesign') }}
</span>
</div>
<!-- <div class="productImg_right_item" v-for="item,index in generateList" :key="item"> -->
<!-- <div class="productImg_right_item_imgBox" v-if="item?.status != 'Success' && !item.imgUrl">
<img class="loadingImg" src="@/assets/images/homePage/loading.gif" alt="">
</div>
<div class="productImg_right_item_imgBox" v-else>
<img :src="item.url" alt="">
<div class="productImg_right_item_iconRight">
<div class="productImg_right_item_like" @click.stop="likeFile(item,'like',index)">
<i class="fi fi-rr-heart"></i>
</div>
</div>
<div class="productImg_right_item_iconLeft" >
<div class="productImg_right_item_scale" @click.stop="setScaleImage(generateList,index,true)">
<i class="fi fi-bs-expand-arrows-alt"></i>
</div>
<div v-show="item.resultType != 'Relight'" class="productImg_right_item_men" @click.stop="setMenuShow(item)">
<i class="fi fi-rr-circle-ellipsis"></i>
<ul v-show="item.menuShow">
<li v-for="menuItem,index in productimgMenuList" v-show="index != 0" @click.stop="setMenu(menuItem,item)" :key="menuItem.value">{{ menuItem.label }}</li>
</ul>
</div>
</div>
</div> -->
<div class="content">
<generalDrag ref="generalDragLeft" @setBtn="selectSetBtn" :isDelete="false" :isLike="true" :list="likeList[productimgMenu.value]"></generalDrag>
<generalDrag ref="generalDragLeft" :type="productimgMenu.value" @setBtn="selectSetBtn" :isDelete="false" :isLike="true" :list="likeList[productimgMenu.value]"></generalDrag>
</div>
<!-- </div> -->
@@ -180,30 +167,8 @@
</span>
</div>
<div class="content">
<generalDrag ref="generalDragRight" @setBtn="generateSetBtn" :list="generateList" :showMark="isShowMark"></generalDrag>
<generalDrag ref="generalDragRight" :type="productimgMenu.value" @setBtn="generateSetBtn" :list="generateList" :showMark="isShowMark"></generalDrag>
</div>
<!-- <div class="mark_loading" v-show="isShowMark">
<a-spin size="large" />
</div> -->
<!-- <div class="productImg_right_item" v-for="item,index in likeList[productimgMenu.value]" :key="item" >
<img :src="item.url" alt="">
<div class="productImg_right_item_iconRight">
<div class="productImg_right_item_like" @click.stop="likeFile(item,'noLike',index)">
<i class="fi fi-sr-heart"></i>
</div>
</div>
<div class="productImg_right_item_iconLeft">
<div class="productImg_right_item_scale" @click.stop="setScaleImage(likeList[productimgMenu.value],index,false)">
<i class="fi fi-bs-expand-arrows-alt"></i>
</div>
<div v-show="item.resultType != 'Relight'" class="productImg_right_item_menu" :title="$t('ProductImg.moreTitle')" @click.stop="setMenuShow(item)">
<i class="fi fi-rr-circle-ellipsis"></i>
<ul v-show="item.menuShow">
<li v-for="menuItem,index in productimgMenuList" v-show="index != 0" @click.stop="setMenu(menuItem,item)" :key="menuItem.value">{{ menuItem.label }}</li>
</ul>
</div>
</div>
</div> -->
</div>
</div>
</div>
@@ -223,7 +188,7 @@
<script lang="ts">
import { LoadingOutlined } from "@ant-design/icons-vue";
import { defineComponent,watch,createVNode, h, ref ,toRefs,computed,reactive,triggerRef, nextTick, inject} from "vue";
import { defineComponent,watch,createVNode, h, ref ,toRefs,computed,reactive,triggerRef, nextTick, inject, onMounted} from "vue";
import { Https } from "@/tool/https";
import { getCookie,setCookie } from "@/tool/cookie";
// import domTurnImg from '@/tool/domTurnImg'
@@ -238,7 +203,7 @@ import generalMenu from "@/component/HomePage/generalMenu.vue";
import UpgradePlan from "@/component/HomePage/UpgradePlan.vue";
import generalDrag from '@/component/modules/generalDrag.vue';
import { List } from "echarts";
import { useRouter,useRoute } from 'vue-router'
export default defineComponent({
components:{
@@ -252,10 +217,13 @@ export default defineComponent({
return store.state.UserHabit.userDetail
})
const {t} = useI18n()
const upload:any = ref({})
const upload:any = ref({
projectId:computed(()=>store.state.Workspace.probjects.id)
})
let driver__:any = computed(()=>{
return store.state.Guide.guide
})
const route = useRoute()
const createProbject:any = inject('createProbject')
let productImgData:any = reactive({
isShowMark:false,
@@ -279,7 +247,43 @@ export default defineComponent({
button:{
left:false,
right:false,
}
},
})
let speed = reactive({
speedList:[
{
title:'Generate with high quality',
label:'High',
relightLabel:'Relight',
value:'',
},{
title:'',
label:'FLUX',
relightLabel:'Edit',
value:'flux',
},
],
speedState:false,
speedData:{
title:'Generate with high quality',
relightLabel:'Relight',
label:'High',
value:'',
},
})
//watch立即执行一次
// watch(()=>productImgData.selectObject.id,(newVal,oldVal)=>{
// createProbject.generateList = []
// },{immediate: true })
watch(() => route.query.id,
(query:any, oldQuery:any) => {
if(oldQuery && query != oldQuery){
productImgData.generateList = []
}
},
);
watch(()=>store.state.HomeStoreModule.uploadElement.length,(newVal,oldVal)=>{
productImgData.fileList[props.productimgMenu.value] = store.state.HomeStoreModule.uploadElement
})
const productImgDom = reactive({
generalDragLeft:null as any,
@@ -326,13 +330,10 @@ export default defineComponent({
}else if(props.productimgMenu.value == 'Relight'){
selectList.value['Relight'] = JSON.parse(JSON.stringify(selectDesignList.value.toProduct))
}
upload.value = {
projectId:productImgData.selectObject.id
}
userlikeGroupId = selectDesignList.value.userlikeGroupId
// getLikeProductImage(selectDesignList.value.userlikeGroupId)
productImgDom.generalDragLeft.setItemPosition()
setCloudImg()
}
const setCloudImg = ()=>{
@@ -430,6 +431,18 @@ export default defineComponent({
let setGenerate = (item:any)=>{
item.isChecked = !item.isChecked
}
const setUploadDelete = (item:any,index:any)=>{
let value = {
id:item.id
}
Https.axiosPost(Https.httpUrls.toProductImageElementDelete,{},{params:value}).then((rv)=>{
let storeData = {
str:'delete',
index,
}
store.commit('setUploadElement',storeData)
})
}
let likeFile = (item:any,str:any,index:any) =>{
let url
let data = {
@@ -492,7 +505,7 @@ export default defineComponent({
productImgData.fileList[props.productimgMenu.value].forEach((item:any)=>{
if(item.isChecked){
obj.elementId = item.id
obj.elementType = item.type
obj.elementType = item.type || 'ProductElement'
selectArr.push(JSON.parse(JSON.stringify(obj)))
}
})
@@ -521,14 +534,15 @@ export default defineComponent({
toProductImageVOList:selectArr,
// userLikeGroupId:upload.value.userlikeGroupId,
projectId:productImgData.selectObject.id,
modelName:speed.speedData.value,
direction:RelightDirection.value,
brightenValue:productImgData.brightenValue,
imageStrength:(100 - imageStrength)/100,
}
productImgData.isProductimg = true
remPrductimgTime = setTimeout(()=>{
productImgData.remProductimg = true
},10000)
// remPrductimgTime = setTimeout(()=>{
// productImgData.remProductimg = true
// },10000)
let url = Https.httpUrls.toProduct
if(props.productimgMenu.value == 'Relight'){
url = Https.httpUrls.relight
@@ -536,12 +550,12 @@ export default defineComponent({
productImgData.isShowMark = true
Https.axiosPost(url, data).then(
(rv) => {
productImgData.remProductimg = true
productImgData.isShowMark = false
let arr:any = []
rv.forEach((item:any)=>{
arr.push(item.taskId)
})
productImgData.generateList.unshift(...rv)
setPrductimg(arr)
}
@@ -699,6 +713,8 @@ export default defineComponent({
const setLikeZoom = (item:any,str:string,index:number,list:List) =>{
if(str == 'zoom'){
setScaleImage(list,index,true)
}else if(str == 'copy'){
setCopy(item)
}else{
likeFile(item,str,index)
}
@@ -707,13 +723,12 @@ export default defineComponent({
productImgData.likeList[props.productimgMenu.value].forEach((item:any,index:number)=>{
if(item.id == id){
let selectStr = ''
str == 'zoom'?selectStr = 'zoom':selectStr ='noLike'
setLikeZoom(item,selectStr,index,productImgData.likeList[props.productimgMenu.value])
str == 'like'?(str = 'noLike'):str
setLikeZoom(item,str,index,productImgData.likeList[props.productimgMenu.value])
}
})
}
const generateSetBtn = (id:any,str:string)=>{
console.log(str)
if(str == 'delete'){
productImgData.generateList = productImgData.generateList.filter((v:any) => v.id != id);
}else{
@@ -721,12 +736,21 @@ export default defineComponent({
if(item.id == id){
let selectStr = ''
str == 'zoom'?selectStr = 'zoom':selectStr ='like'
setLikeZoom(item,selectStr,index,productImgData.generateList)
setLikeZoom(item,str,index,productImgData.generateList)
}
})
}
}
const setCopy = (item:any)=>{
let value = {
id:item.id
}
Https.axiosPost(Https.httpUrls.convertRelightElement,{},{params:value}).then((rv)=>{
if(!productImgData.fileList[props.productimgMenu.value]?.length)productImgData.fileList[props.productimgMenu.value] = []
productImgData.fileList[props.productimgMenu.value].unshift(rv)
})
}
const setItemPosition = ()=>{
productImgDom.generalDragLeft.setItemPosition()
productImgDom.generalDragRight.setItemPosition()
@@ -749,94 +773,109 @@ export default defineComponent({
textarea.style.height = newHeight + 'px';
textarea.scrollTop = scrollTop;
}
const openSpeed = ()=>{
speed.speedState = !speed.speedState
if(speed.speedState){
document.addEventListener('click',openSpeed)
}else{
document.removeEventListener('click',openSpeed)
}
}
const setSpeed = (item:any)=>{
speed.speedData = item
speed.speedState = false
}
return {
upload,
driver__,
openSetData,
...toRefs(productImgData),
...toRefs(productImgDom),
...toRefs(speed),
productimgMenuList,
RelightDirectionList,
RelightDirection,
RelightDirectionList,
RelightDirection,
selectList,
setproduct,
fileUploadChange,
beforeUpload,
deleteFile,
setGenerate,
likeFile,
getPrductimg,
removeProductimg,
scaleImage,
setScaleImage,
setMenu,
setMenuShow,
setSimilarity,
setTask,
selectSetBtn,
generateSetBtn,
setItemPosition,
setSize,
ifMaximumLength,
};
},
directives:{
mousewheel:{
mounted (el) {
el.addEventListener('wheel',(e:WheelEvent)=>{
let num = 0
if(e.deltaY > 0){
num = 25
}else{
num = -25
}
el.scrollBy(num, 0);
},{ passive: true })
}
},
},
data() {
return {
indicator: h(LoadingOutlined, {
style: {
fontSize: "2.4rem",
selectList,
setproduct,
fileUploadChange,
beforeUpload,
deleteFile,
setGenerate,
setUploadDelete,
likeFile,
getPrductimg,
removeProductimg,
scaleImage,
setScaleImage,
setMenu,
setMenuShow,
setSimilarity,
setTask,
selectSetBtn,
generateSetBtn,
setItemPosition,
setSize,
ifMaximumLength,
openSpeed,
setSpeed,
};
},
directives:{
mousewheel:{
mounted (el) {
el.addEventListener('wheel',(e:WheelEvent)=>{
let num = 0
if(e.deltaY > 0){
num = 25
}else{
num = -25
}
el.scrollBy(num, 0);
},{ passive: true })
}
},
spin: true,
}),
// moodTemplateId: "", //模板id
token: "",
uploadUrl: "",
};
},
mounted() {
this.token = getCookie("token") || "";
this.uploadUrl = getUploadUrl();
},
methods: {
// init(list:any,index:any,dialogueIndex:any){
// },
// cancelDsign(){
// this.productImg = false
// // this.productImgList = []
// // this.productImgIndex = 0
// },
// download(){
// // downloadIamge(this.productImgList[this.productImgIndex].imgUrl)
// },
// setScaleImageIndex(index:any){
// // this.productImgIndex = index
// // console.log(this.productImgIndex);
// },
// LikeFile(item:any,str:string){
// let parent:any = this.$parent
// parent.likeFile(item,str)
// },
},
},
data() {
return {
indicator: h(LoadingOutlined, {
style: {
fontSize: "2.4rem",
},
spin: true,
}),
// moodTemplateId: "", //模板id
token: "",
uploadUrl: "",
};
},
mounted() {
this.token = getCookie("token") || "";
this.uploadUrl = getUploadUrl();
},
methods: {
// init(list:any,index:any,dialogueIndex:any){
// },
// cancelDsign(){
// this.productImg = false
// // this.productImgList = []
// // this.productImgIndex = 0
// },
// download(){
// // downloadIamge(this.productImgList[this.productImgIndex].imgUrl)
// },
// setScaleImageIndex(index:any){
// // this.productImgIndex = index
// // console.log(this.productImgIndex);
// },
// LikeFile(item:any,str:string){
// let parent:any = this.$parent
// parent.likeFile(item,str)
// },
},
});
</script>
@@ -890,59 +929,145 @@ methods: {
.productImg_content_item_intro{
font-size: 1.4rem;
}
.productImg_content_item_imgBox{
display: flex;
overflow-x: auto;
width: auto;
margin-bottom: 2rem;
align-items: center;
flex-wrap: nowrap;
.content_item_imgBox_itemImg{
display: flex;
margin-right: 1rem;
position: relative;
height: 36rem;
max-height: 100%;
position: relative;
img{
height: 100%;
width: 100%;
cursor: pointer;
object-fit: contain;
opacity: .5;
&.active{
opacity: 1;
}
}
.ant-checkbox-wrapper{
position: absolute;
right: 0;
top: 0;
}
&.content_item_imgBox_itemImg:hover{
.content_item_imgBox_itemImg_delete{
display: block;
cursor: pointer;
}
}
.content_item_imgBox_itemImg_delete{
display: none;
width: 100%;
height: 100%;
background: rgba(0,0,0,.2);
position: absolute;
i{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
color: #fff;
}
}
// .productImg_content_item_imgBox{
// display: flex;
// overflow-x: auto;
// width: auto;
// margin-bottom: 2rem;
// align-items: center;
// flex-wrap: nowrap;
// .content_item_imgBox_itemImg{
// display: flex;
// margin-right: 1rem;
// position: relative;
// height: 36rem;
// max-height: 100%;
// position: relative;
// img{
// height: 100%;
// width: 100%;
// cursor: pointer;
// object-fit: contain;
// opacity: .5;
// &.active{
// opacity: 1;
// }
// }
// .ant-checkbox-wrapper{
// position: absolute;
// right: 0;
// top: 0;
// }
// &.content_item_imgBox_itemImg:hover{
// .content_item_imgBox_itemImg_delete{
// display: block;
// cursor: pointer;
// }
// }
// .content_item_imgBox_itemImg_delete{
// display: none;
// width: 100%;
// height: 100%;
// background: rgba(0,0,0,.2);
// position: absolute;
// i{
// position: absolute;
// left: 50%;
// top: 50%;
// transform: translate(-50%,-50%);
// color: #fff;
// }
// }
// }
// .content_item_imgBox_itemImg:last-child{
// margin-right: 0;
// }
// }
.selectImg{
display: flex;
flex-direction: column;
> .imgBox{
flex: 1;
max-height: 45rem;
margin-top: 2rem;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
overflow: hidden;
overflow-x: auto;
// &.active
> .item{
margin-right: 1rem;
width: calc(100% / 2 - .5rem);
cursor: pointer;
overflow: hidden;
display: flex;
justify-content: center;
flex-shrink: 0;
height: 25rem;
position: relative;
// &.active{
// border: 2px solid;
// }
:deep(.ant-checkbox-wrapper){
position: absolute;
top: 2rem;
right: 2rem;
}
> .btnBox{
position: absolute;
right: 1rem;
top: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
> div{
width: 2rem;
height: 2rem;
display: flex;
border: 1px solid;
align-items: center;
border-radius: .5rem;
justify-content: center;
cursor: pointer;
margin-bottom: 1rem;
> i{
display: flex;
}
>.fi-br-check{
color: #fff;
display: none;
}
&.active{
background: #000;
> i{
display: flex;
color: #fff;
}
}
}
}
> img{
width: 100%;
object-fit: contain;
}
}
> .upload_item{
border: none;
}
}
.content_item_imgBox_itemImg:last-child{
margin-right: 0;
> .head{
color: #000;
font-weight: 600;
> .text{
display: inline-block;
font-size: 1.6rem;
}
}
}
.productImg_left,.productImg_right{

View File

@@ -27,6 +27,9 @@
<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'" @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')">
<i class="fi fi-rr-trash icon_delete"></i>
</div>
@@ -51,6 +54,7 @@ export default defineComponent({
showMark:{type:Boolean,default:false},
isLike:{type:Boolean,default:false},
isDelete:{type:Boolean,default:true},
type:{type:String,default:''},
},
emits:['setBtn','setSort'],
setup(props,{emit}) {