feat: 裁剪组裁剪跟随选择组移动

This commit is contained in:
bighuixiang
2025-07-14 01:00:23 +08:00
parent 96e13cb22a
commit 24e9ba8ae5
80 changed files with 2052 additions and 4292 deletions

View File

@@ -81,11 +81,7 @@ export class LayerSort {
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (!layer.isBackground && !layer.isFixed) {
// 普通图层
currentZIndex = this.processLayerObjects(
layer,
currentZIndex,
zIndexMap
);
currentZIndex = this.processLayerObjects(layer, currentZIndex, zIndexMap);
}
}
@@ -142,8 +138,7 @@ export class LayerSort {
getChildLayersInOrder(parentLayerId) {
// 获取所有子图层
const childLayers =
this.layers.value.filter((layer) => layer.id === parentLayerId)
?.children || [];
this.layers.value.filter((layer) => layer.id === parentLayerId)?.children || [];
return childLayers;
}
@@ -194,9 +189,7 @@ export class LayerSort {
for (let i = start; i < end; i++) {
const item = sortedObjects[i];
const currentIndex = this.canvas
.getObjects()
.indexOf(item.object);
const currentIndex = this.canvas.getObjects().indexOf(item.object);
if (currentIndex !== i && currentIndex !== -1) {
this.canvas.moveTo(item.object, i);
}
@@ -288,23 +281,17 @@ export class LayerSort {
return this.layers.value.length; // 背景图层插入到最后
} else if (newLayer.isFixed) {
// 固定图层插入到背景图层之前
const bgIndex = this.layers.value.findIndex(
(layer) => layer.isBackground
);
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
);
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
);
const targetIndex = this.layers.value.findIndex((layer) => layer.id === targetLayerId);
return targetIndex !== -1 ? targetIndex : this.layers.value.length;
}
@@ -532,9 +519,7 @@ export class LayerSort {
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
);
: this.layers.value.filter((layer) => !layer.isBackground && !layer.isFixed);
if (layersToSort.length <= 1) return true;
@@ -556,9 +541,7 @@ export class LayerSort {
// 更新图层顺序
const sortedLayerIds = layersToSort.map((layer) => layer.id);
const otherLayers = this.layers.value.filter(
(layer) => !sortedLayerIds.includes(layer.id)
);
const otherLayers = this.layers.value.filter((layer) => !sortedLayerIds.includes(layer.id));
// 重新组织图层数组:保持背景层和固定层的位置
const newLayers = [];
@@ -733,12 +716,9 @@ export const LayerSortUtils = {
* @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];
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];
},

View File

@@ -9,7 +9,6 @@ export const createCanvas = (elementId, options = {}) => {
const canvas = new fabric.Canvas(elementId, {
enableRetinaScaling: canvasConfig.enableRetinaScaling,
renderOnAddRemove: false,
enableRetinaScaling: true,
preserveObjectStacking: true, // 保持对象堆叠顺序
// skipOffscreen: true, // 跳过离屏渲染
imageSmoothingEnabled: true, // 启用图像平滑 - 抗锯齿

View File

@@ -6,12 +6,7 @@ export function deepCompare(obj1, obj2) {
return null;
}
if (
obj1 === null ||
obj2 === null ||
typeof obj1 !== "object" ||
typeof obj2 !== "object"
) {
if (obj1 === null || obj2 === null || typeof obj1 !== "object" || typeof obj2 !== "object") {
return { _value: obj2, _oldValue: obj1 };
}
@@ -78,7 +73,7 @@ export function deepClone(obj) {
if (typeof obj === "object") {
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key]);
}
}
@@ -228,11 +223,7 @@ export function formatDuration(milliseconds) {
* @returns {boolean} 是否有效
*/
export function isValidCommand(command) {
return (
command &&
typeof command === "object" &&
typeof command.execute === "function"
);
return command && typeof command === "object" && typeof command.execute === "function";
}
/**
@@ -286,7 +277,7 @@ export function getObjectDepth(obj) {
let maxDepth = 0;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const depth = getObjectDepth(obj[key]);
maxDepth = Math.max(maxDepth, depth);
}
@@ -313,8 +304,7 @@ export function checkBrowserSupport() {
return {
WeakRef: typeof WeakRef !== "undefined",
FinalizationRegistry: typeof FinalizationRegistry !== "undefined",
PerformanceMemory:
typeof performance !== "undefined" && !!performance.memory,
PerformanceMemory: typeof performance !== "undefined" && !!performance.memory,
RequestIdleCallback: typeof requestIdleCallback !== "undefined",
IntersectionObserver: typeof IntersectionObserver !== "undefined",
ResizeObserver: typeof ResizeObserver !== "undefined",
@@ -337,12 +327,7 @@ export function delay(ms) {
* @returns {Promise} 执行结果
*/
export async function retry(fn, options = {}) {
const {
retries = 3,
delay: delayMs = 1000,
backoff = 1.5,
shouldRetry = () => true,
} = options;
const { retries = 3, delay: delayMs = 1000, backoff = 1.5, shouldRetry = () => true } = options;
let attempt = 0;
let currentDelay = delayMs;
@@ -377,9 +362,7 @@ export async function batchProcess(items, processor, options = {}) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => processor(item))
);
const batchResults = await Promise.all(batch.map((item) => processor(item)));
results.push(...batchResults);

View File

@@ -35,14 +35,8 @@ function initAligningGuidelines(canvas) {
ctx.lineWidth = aligningLineWidth;
ctx.strokeStyle = aligningLineColor;
ctx.beginPath();
ctx.moveTo(
x1 * zoom + viewportTransform[4],
y1 * zoom + viewportTransform[5]
);
ctx.lineTo(
x2 * zoom + viewportTransform[4],
y2 * zoom + viewportTransform[5]
);
ctx.moveTo(x1 * zoom + viewportTransform[4], y1 * zoom + viewportTransform[5]);
ctx.lineTo(x2 * zoom + viewportTransform[4], y2 * zoom + viewportTransform[5]);
ctx.stroke();
ctx.restore();
}
@@ -50,11 +44,7 @@ function initAligningGuidelines(canvas) {
function isInRange(value1, value2) {
value1 = Math.round(value1);
value2 = Math.round(value2);
for (
var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin;
i <= len;
i++
) {
for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) {
if (i === value2) {
return true;
}
@@ -77,8 +67,7 @@ function initAligningGuidelines(canvas) {
activeObjectLeft = activeObjectCenter.x,
activeObjectTop = activeObjectCenter.y,
activeObjectBoundingRect = activeObject.getBoundingRect(),
activeObjectHeight =
activeObjectBoundingRect.height / viewportTransform[3],
activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3],
activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0],
horizontalInTheRange = false,
verticalInTheRange = false,
@@ -100,12 +89,7 @@ function initAligningGuidelines(canvas) {
objectWidth = objectBoundingRect.width / viewportTransform[0];
// snaps if the right side of the active object touches the left side of the object
if (
isInRange(
activeObjectLeft + activeObjectWidth / 2,
objectLeft - objectWidth / 2
)
) {
if (isInRange(activeObjectLeft + activeObjectWidth / 2, objectLeft - objectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
@@ -120,22 +104,14 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft - objectWidth / 2 - activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
}
// snaps if the left side of the active object touches the right side of the object
if (
isInRange(
activeObjectLeft - activeObjectWidth / 2,
objectLeft + objectWidth / 2
)
) {
if (isInRange(activeObjectLeft - activeObjectWidth / 2, objectLeft + objectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
@@ -150,22 +126,14 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft + objectWidth / 2 + activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
}
// snaps if the bottom of the object touches the top of the active object
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
if (isInRange(objectTop + objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
@@ -180,22 +148,14 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 + activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 + activeObjectHeight / 2),
"center",
"center"
);
}
// snaps if the top of the object touches the bottom of the active object
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
if (isInRange(objectTop - objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
@@ -210,10 +170,7 @@ function initAligningGuidelines(canvas) {
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 - activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 - activeObjectHeight / 2),
"center",
"center"
);
@@ -241,12 +198,7 @@ function initAligningGuidelines(canvas) {
}
// snap by the left edge
if (
isInRange(
objectLeft - objectWidth / 2,
activeObjectLeft - activeObjectWidth / 2
)
) {
if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
@@ -260,22 +212,14 @@ function initAligningGuidelines(canvas) {
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
}
// snap by the right edge
if (
isInRange(
objectLeft + objectWidth / 2,
activeObjectLeft + activeObjectWidth / 2
)
) {
if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
@@ -289,10 +233,7 @@ function initAligningGuidelines(canvas) {
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop),
"center",
"center"
);
@@ -320,12 +261,7 @@ function initAligningGuidelines(canvas) {
}
// snap by the top edge
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
@@ -339,22 +275,14 @@ function initAligningGuidelines(canvas) {
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 + activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2),
"center",
"center"
);
}
// snap by the bottom edge
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
@@ -368,10 +296,7 @@ function initAligningGuidelines(canvas) {
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 - activeObjectHeight / 2
),
new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2),
"center",
"center"
);
@@ -397,7 +322,7 @@ function initAligningGuidelines(canvas) {
for (var i = verticalLines.length; i--; ) {
drawVerticalLine(verticalLines[i]);
}
for (var i = horizontalLines.length; i--; ) {
for (let i = horizontalLines.length; i--; ) {
drawHorizontalLine(horizontalLines[i]);
}
@@ -433,16 +358,14 @@ export function initCenteringGuidelines(canvas) {
viewportTransform;
for (
var i = canvasWidthCenter - centerLineMargin,
len = canvasWidthCenter + centerLineMargin;
var i = canvasWidthCenter - centerLineMargin, len = canvasWidthCenter + centerLineMargin;
i <= len;
i++
) {
canvasWidthCenterMap[Math.round(i)] = true;
}
for (
var i = canvasHeightCenter - centerLineMargin,
len = canvasHeightCenter + centerLineMargin;
let i = canvasHeightCenter - centerLineMargin, len = canvasHeightCenter + centerLineMargin;
i <= len;
i++
) {
@@ -450,21 +373,11 @@ export function initCenteringGuidelines(canvas) {
}
function showVerticalCenterLine() {
showCenterLine(
canvasWidthCenter + 0.5,
0,
canvasWidthCenter + 0.5,
canvasHeight
);
showCenterLine(canvasWidthCenter + 0.5, 0, canvasWidthCenter + 0.5, canvasHeight);
}
function showHorizontalCenterLine() {
showCenterLine(
0,
canvasHeightCenter + 0.5,
canvasWidth,
canvasHeightCenter + 0.5
);
showCenterLine(0, canvasHeightCenter + 0.5, canvasWidth, canvasHeightCenter + 0.5);
}
function showCenterLine(x1, y1, x2, y2) {
@@ -493,9 +406,8 @@ export function initCenteringGuidelines(canvas) {
if (!transform) return;
(isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap),
(isInHorizontalCenter =
Math.round(objectCenter.y) in canvasHeightCenterMap);
((isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap),
(isInHorizontalCenter = Math.round(objectCenter.y) in canvasHeightCenterMap));
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(

View File

@@ -1,13 +1,12 @@
/* eslint-disable no-async-promise-executor */
import { fabric } from "fabric-with-all";
import { LayerType, OperationType, createBitmapLayer } from "./layerHelper";
// 导入新的复合命令
import { CreateImageLayerCommand } from "../commands/LayerCommands";
// 导入新的命令
import {
ChangeFixedImageCommand,
AddImageToLayerCommand,
} from "../commands/LayerCommands";
import { ChangeFixedImageCommand, AddImageToLayerCommand } from "../commands/LayerCommands";
import { generateId } from "./helper";
import { isBoolean } from "lodash-es";
/**
* 加载并处理图片
@@ -101,9 +100,7 @@ export async function createImageLayer({
if (isBoolean(undoable)) createImageLayerCmd.undoable = undoable; // 是否撤销
// 执行复合命令
const newLayerId = await layerManager.commandManager.execute(
createImageLayerCmd
);
const newLayerId = await layerManager.commandManager.execute(createImageLayerCmd);
return newLayerId;
} catch (error) {
@@ -120,11 +117,7 @@ export async function createImageLayer({
* @param {Object} options.fabricImage - 新的图像对象
* @returns {Promise<boolean>} 是否成功更改
*/
export async function changeFixedImage({
layerManager,
fixedLayerId,
fabricImage,
} = {}) {
export async function changeFixedImage({ layerManager, fixedLayerId, fabricImage } = {}) {
if (!layerManager || !fixedLayerId || !fabricImage) {
console.error("更改固定图层图像:参数无效");
return false;
@@ -141,9 +134,7 @@ export async function changeFixedImage({
});
// 通过命令管理器执行
const result = await layerManager.commandManager.execute(
changeFixedImageCmd
);
const result = await layerManager.commandManager.execute(changeFixedImageCmd);
if (result) {
console.log(`✅ 成功更改固定图层 "${fixedLayerId}" 的图像`);
@@ -192,9 +183,7 @@ export async function addImageToLayer({
});
// 通过命令管理器执行
const resultLayerId = await layerManager.commandManager.execute(
addImageToLayerCmd
);
const resultLayerId = await layerManager.commandManager.execute(addImageToLayerCmd);
if (resultLayerId) {
if (targetLayerId) {
@@ -219,10 +208,7 @@ export async function addImageToLayer({
* @param {Object} options - 配置选项
* @returns {Promise<string>} 新图层ID的Promise
*/
export function loadImageUrlToLayer(
{ imageUrl, layerManager, canvas, toolManager },
options = {}
) {
export function loadImageUrlToLayer({ imageUrl, layerManager, canvas, toolManager }, options = {}) {
return new Promise(async (resolve, reject) => {
if (!imageUrl || !layerManager || !canvas) {
reject(new Error("参数无效"));
@@ -231,9 +217,7 @@ export function loadImageUrlToLayer(
try {
// 查找背景图层以获取尺寸
const bgLayer = layerManager.layers.value.find(
(layer) => layer.isBackground
);
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
// 设置最大宽高为背景图层的尺寸
const maxWidth = bgLayer?.canvasWidth || canvas.width;
@@ -304,9 +288,7 @@ export function uploadImageAndCreateLayer(
reader.onload = async (e) => {
try {
// 查找背景图层以获取尺寸
const bgLayer = layerManager.layers.value.find(
(layer) => layer.isBackground
);
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
// 设置最大宽高为背景图层的尺寸
const maxWidth = bgLayer?.canvasWidth || canvas.width;
@@ -361,9 +343,7 @@ export function safeLoadImage(imageSource, options = {}) {
.then(resolve)
.catch((error) => {
if (attempt < retries) {
console.warn(
`图片加载失败,正在重试 (${attempt + 1}/${retries})...`
);
console.warn(`图片加载失败,正在重试 (${attempt + 1}/${retries})...`);
setTimeout(() => attemptLoad(attempt + 1), 500);
} else {
reject(error);
@@ -426,12 +406,7 @@ export function loadImageAndChangeFixedLayer({
* @param {Object} options.imageOptions - 图片加载选项
* @returns {Promise<string>} 新图像对象ID的Promise
*/
export function uploadImageAndChangeFixedLayer({
file,
layerManager,
layerId,
imageOptions = {},
}) {
export function uploadImageAndChangeFixedLayer({ file, layerManager, layerId, imageOptions = {} }) {
return new Promise((resolve, reject) => {
if (!file || !layerManager || !layerId) {
reject(new Error("参数无效需要文件、图层管理器和图层ID"));
@@ -449,9 +424,7 @@ export function uploadImageAndChangeFixedLayer({
reader.onload = async (e) => {
try {
// 查找目标固定图层以获取尺寸信息
const targetLayer = layerManager.layers.value.find(
(layer) => layer.id === layerId
);
const targetLayer = layerManager.layers.value.find((layer) => layer.id === layerId);
if (!targetLayer) {
throw new Error(`找不到图层 ID: ${layerId}`);
@@ -463,9 +436,7 @@ export function uploadImageAndChangeFixedLayer({
}
// 查找背景图层以获取画布尺寸
const bgLayer = layerManager.layers.value.find(
(layer) => layer.isBackground
);
const bgLayer = layerManager.layers.value.find((layer) => layer.isBackground);
const maxWidth = bgLayer?.canvasWidth || layerManager.canvas.width;
const maxHeight = bgLayer?.canvasHeight || layerManager.canvas.height;
@@ -490,14 +461,10 @@ export function uploadImageAndChangeFixedLayer({
});
// 通过命令管理器执行
const newImageId = await layerManager.commandManager.execute(
changeFixedImageCmd
);
const newImageId = await layerManager.commandManager.execute(changeFixedImageCmd);
if (newImageId) {
console.log(
`✅ 成功更改固定图层 "${targetLayer.name}" 的图像新图像ID: ${newImageId}`
);
console.log(`✅ 成功更改固定图层 "${targetLayer.name}" 的图像新图像ID: ${newImageId}`);
resolve(newImageId);
} else {
throw new Error("更改固定图层图像失败");
@@ -826,9 +793,7 @@ export class AdvancedImageManager {
total: imageUrls.length,
successful: results.filter((r) => r.success).length,
failed: results.filter((r) => !r.success).length,
cacheHitRate:
this.performanceMetrics.cacheHits /
this.performanceMetrics.imageLoads,
cacheHitRate: this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads,
averageLoadTime: this.performanceMetrics.averageLoadTime,
},
};
@@ -884,13 +849,10 @@ export class AdvancedImageManager {
// 立即执行模式
for (const operation of operations) {
try {
const result = await this.canvasManager.changeFixedImage(
operation.imageUrl,
{
targetLayerType: operation.layerType,
...operation.options,
}
);
const result = await this.canvasManager.changeFixedImage(operation.imageUrl, {
targetLayerType: operation.layerType,
...operation.options,
});
operationResults.push({ success: true, ...result, operation });
} catch (error) {
operationResults.push({
@@ -1038,10 +1000,7 @@ export class AdvancedImageManager {
}
// 替换模板中的变量
const operations = this.replaceTemplateVariables(
template.operations,
variables
);
const operations = this.replaceTemplateVariables(template.operations, variables);
// 执行操作
const result = await this.batchAddImagesToLayers(operations);
@@ -1141,8 +1100,7 @@ export class AdvancedImageManager {
this.performanceMetrics.imageLoads++;
this.performanceMetrics.totalLoadTime += loadTime;
this.performanceMetrics.averageLoadTime =
this.performanceMetrics.totalLoadTime /
this.performanceMetrics.imageLoads;
this.performanceMetrics.totalLoadTime / this.performanceMetrics.imageLoads;
}
getSummary(results) {
@@ -1170,12 +1128,9 @@ export class AdvancedImageManager {
// 替换字符串中的变量 {{variable}}
Object.keys(newOp).forEach((key) => {
if (typeof newOp[key] === "string") {
newOp[key] = newOp[key].replace(
/\{\{(\w+)\}\}/g,
(match, varName) => {
return variables[varName] || match;
}
);
newOp[key] = newOp[key].replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] || match;
});
}
});
@@ -1223,10 +1178,7 @@ export class AdvancedImageManager {
generatePerformanceRecommendations() {
const recommendations = [];
if (
this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads <
0.3
) {
if (this.performanceMetrics.cacheHits / this.performanceMetrics.imageLoads < 0.3) {
recommendations.push("考虑增加缓存大小以提高缓存命中率");
}
@@ -1298,12 +1250,7 @@ export const ImageUtils = {
* @param {string} targetLayerId - 目标图层ID (可选)
* @returns {Promise<Object>} 执行结果
*/
quickAddImageToLayer: (
file,
layerManager,
toolManager,
targetLayerId = null
) => {
quickAddImageToLayer: (file, layerManager, toolManager, targetLayerId = null) => {
return uploadImageAndAddToLayerSimple({
file,
layerManager,
@@ -1376,12 +1323,7 @@ export function rasterizeCanvasObjects(options = {}) {
function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
return new Promise((resolve, reject) => {
try {
const {
trimWhitespace = true,
trimPadding = 10,
quality = 1,
format = "png",
} = options;
const { trimWhitespace = true, trimPadding = 10, quality = 1, format = "png" } = options;
// 保存原始状态
const originalObjects = canvas.getObjects();
@@ -1389,9 +1331,7 @@ function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
const originalZoom = canvas.getZoom();
// 临时隐藏其他对象,只显示要栅格化的对象
const objectsToHide = originalObjects.filter(
(obj) => !objects.includes(obj)
);
const objectsToHide = originalObjects.filter((obj) => !objects.includes(obj));
// 隐藏不需要的对象
objectsToHide.forEach((obj) => {
@@ -1415,9 +1355,7 @@ function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
const pixelRatio = canvas.getRetinaScaling();
// 复制画布元素(这会保持所有变换状态)
const copiedCanvas = fabric.util.copyCanvasElement(
canvas.lowerCanvasEl
);
const copiedCanvas = fabric.util.copyCanvasElement(canvas.lowerCanvasEl);
let finalCanvas = copiedCanvas;
let trimOffset = { x: 0, y: 0 };
@@ -1496,7 +1434,7 @@ function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
*/
function _restoreObjectVisibility(objects) {
objects.forEach((obj) => {
if (obj.hasOwnProperty("_originalVisible")) {
if (Object.prototype.hasOwnProperty.call(obj, "_originalVisible")) {
obj.set("visible", obj._originalVisible);
delete obj._originalVisible;
}
@@ -1521,9 +1459,7 @@ function _rasterizeUsingDataURL(canvas, objects, options = {}) {
const originalObjects = canvas.getObjects();
// 临时移除其他对象
const objectsToRemove = originalObjects.filter(
(obj) => !objects.includes(obj)
);
const objectsToRemove = originalObjects.filter((obj) => !objects.includes(obj));
objectsToRemove.forEach((obj) => {
canvas.remove(obj);
});
@@ -1770,12 +1706,7 @@ function _trimCanvas(canvas, padding = 0) {
trimmedCanvas.height = trimHeight;
// 复制裁剪区域包含padding
const trimmedImageData = ctx.getImageData(
paddedMinX,
paddedMinY,
trimWidth,
trimHeight
);
const trimmedImageData = ctx.getImageData(paddedMinX, paddedMinY, trimWidth, trimHeight);
trimmedCtx.putImageData(trimmedImageData, 0, 0);
return {
@@ -1955,8 +1886,7 @@ export function smartRasterize(options = {}) {
function _analyzeRasterizationContext(canvas, objects) {
const zoom = canvas.getZoom();
const viewportTransform = canvas.viewportTransform;
const hasTransform =
zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
const hasTransform = zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
// 分析对象类型分布
const objectTypes = objects.reduce((acc, obj) => {
@@ -1971,9 +1901,7 @@ function _analyzeRasterizationContext(canvas, objects) {
// 计算画布利用率
const canvasArea = canvas.width * canvas.height;
const objectsBounds = _calculateObjectsBounds(objects);
const objectsArea = objectsBounds
? objectsBounds.width * objectsBounds.height
: 0;
const objectsArea = objectsBounds ? objectsBounds.width * objectsBounds.height : 0;
const utilization = objectsArea / canvasArea;
return {
@@ -1985,9 +1913,7 @@ function _analyzeRasterizationContext(canvas, objects) {
utilization,
canvasSize: { width: canvas.width, height: canvas.height },
objectsBounds,
supportsCanvasCopy: !!(
fabric.util.copyCanvasElement && canvas.lowerCanvasEl
),
supportsCanvasCopy: !!(fabric.util.copyCanvasElement && canvas.lowerCanvasEl),
};
}
@@ -2223,12 +2149,7 @@ function _getAlternativeMethods(analysis) {
* @param {number} params.canvasWidth - 画布宽度
* @param {number} params.canvasHeight - 画布高度
*/
export const imageModeHandler = ({
imageMode,
newImage,
canvasWidth,
canvasHeight,
}) => {
export const imageModeHandler = ({ imageMode, newImage, canvasWidth, canvasHeight }) => {
switch (imageMode) {
case "stretch":
// 拉伸模式 - 填充整个画布
@@ -2253,10 +2174,8 @@ export const imageModeHandler = ({
// 这里可以添加裁剪逻辑,如果需要的话
// 例如使用fabric.Image.clipPath来裁剪图像
break;
case "contains":
// 包含模式 - 保证图像在画布内完整显示
// 既要考虑画布的宽高比,也要考虑图像的宽高比
// 图片缩放后要保证最长边能完全显示在画布内
case "contains": {
// 图片缩放后要保证最长边能完全显示在画布内 // 既要考虑画布的宽高比,也要考虑图像的宽高比 // 包含模式 - 保证图像在画布内完整显示
const canvasAspect = canvasWidth / canvasHeight;
const imageAspect = newImage.width / newImage.height;
// 保证图像在画布内完整显示 - 既要考虑画布的宽高比,也要考虑图像的宽高比
@@ -2269,5 +2188,6 @@ export const imageModeHandler = ({
newImage.scaleToHeight(canvasHeight);
}
break;
}
}
};

View File

@@ -95,8 +95,7 @@ export const BlendMode = {
export function isGroupLayer(layer) {
if (!layer) return false;
return (
layer.type === LayerType.GROUP ||
(Array.isArray(layer.children) && layer.children.length > 0)
layer.type === LayerType.GROUP || (Array.isArray(layer.children) && layer.children.length > 0)
);
}
@@ -107,11 +106,7 @@ export function isGroupLayer(layer) {
* @param {Object} options 其他选项
* @returns {Object} 创建的图层对象
*/
export function createLayerFromFabricObject(
fabricObject,
layerType = "bitmap",
options = {}
) {
export function createLayerFromFabricObject(fabricObject, layerType = "bitmap", options = {}) {
if (!fabricObject) return null;
// 确定图层类型
@@ -137,9 +132,7 @@ export function createLayerFromFabricObject(
type: type,
name:
options.name ||
`${
fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1)
} 图层`,
`${fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1)} 图层`,
parentId: options.parentId || null,
});
@@ -166,9 +159,7 @@ export function createLayerFromFabricObject(
*/
export function createLayer(options = {}) {
const id =
options.id ||
generateId("layer_") ||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
options.id || generateId("layer_") || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
@@ -208,8 +199,7 @@ export function createLayer(options = {}) {
* @returns {Object} 背景图层对象
*/
export function createBackgroundLayer(options = {}) {
const id =
options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const id = options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
@@ -400,9 +390,7 @@ export function createSmartObjectLayer(options = {}) {
* @returns {Object} 固定图层对象
*/
export function createFixedLayer(options = {}) {
const id =
options.id ||
`fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const id = options.id || `fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
@@ -445,9 +433,7 @@ export function cloneLayer(layer) {
// 复制 fabric 对象 (如果存在)
if (Array.isArray(layer.fabricObjects)) {
clonedLayer.fabricObjects = layer.fabricObjects.map((obj) => {
return obj && typeof obj.clone === "function"
? obj.clone()
: JSON.parse(JSON.stringify(obj));
return obj && typeof obj.clone === "function" ? obj.clone() : JSON.parse(JSON.stringify(obj));
});
}

View File

@@ -15,18 +15,13 @@ export function buildLayerAssociations(layer, canvasObjects) {
// 处理单个fabricObject关联
if (layer.fabricObject) {
// 如果图层已经有关联的fabricObject确保它的layerId和layerName正确
layer.fabricObject =
canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
layer.fabricObject = canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
}
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组 也可能是一个简单对象
const clippingMaskObj = canvasObjects.find(
(obj) => obj.id === layer.clippingMask.id
);
layer.clippingMask = clippingMaskObj
? clippingMaskObj?.toObject?.(["id"])
: layer.clippingMask;
const clippingMaskObj = canvasObjects.find((obj) => obj.id === layer.clippingMask.id);
layer.clippingMask = clippingMaskObj ? clippingMaskObj?.toObject?.(["id"]) : layer.clippingMask;
}
// 处理多个fabricObjects关联
@@ -172,10 +167,7 @@ export function simplifyLayers(layers) {
opacity: layer.opacity,
isBackground: layer.isBackground || false,
isFixed: layer.isFixed || false,
clippingMask:
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
layer.clippingMask ||
null, // 可能是一个fabricObject或组 也可能是一个简单对象
clippingMask: layer.clippingMask?.toObject?.(["id", "layerId"]) || layer.clippingMask || null, // 可能是一个fabricObject或组 也可能是一个简单对象
// ? {
// id: layer.clippingMask.id,
// type: layer.clippingMask.type,
@@ -200,10 +192,7 @@ export function simplifyLayers(layers) {
)
.filter((obj) => obj !== null)
: [],
children:
layer.children && isArray(layer.children)
? simplifyLayers(layer.children)
: [],
children: layer.children && isArray(layer.children) ? simplifyLayers(layer.children) : [],
};
return simplifiedLayer;
@@ -240,9 +229,7 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
if (layer.clippingMask) {
// clippingMask 可能是一个fabricObject或组 也可能是一个简单对象
const clippingMaskObj = canvasObjects.find(
(obj) => obj.id === layer.clippingMask.id
);
const clippingMaskObj = canvasObjects.find((obj) => obj.id === layer.clippingMask.id);
restoredLayer.clippingMask = clippingMaskObj
? clippingMaskObj?.toObject?.(["id"])
: layer.clippingMask;
@@ -250,17 +237,11 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
// 恢复单个fabricObject关联
if (layer.fabricObject?.id) {
const fabricObj = canvasObjects.find(
(obj) => obj.id === layer.fabricObject.id
);
const fabricObj = canvasObjects.find((obj) => obj.id === layer.fabricObject.id);
if (fabricObj) {
fabricObj.layerId = layer.id;
fabricObj.layerName = layer.name;
restoredLayer.fabricObject = fabricObj.toObject([
"id",
"layerId",
"type",
]);
restoredLayer.fabricObject = fabricObj.toObject(["id", "layerId", "type"]);
} else {
restoredLayer.fabricObject = null;
}
@@ -270,9 +251,7 @@ export function restoreLayers(simplifiedLayers, canvasObjects) {
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
restoredLayer.fabricObjects = layer.fabricObjects
.map((fabricRef) => {
const fabricObj = canvasObjects.find(
(obj) => obj.id === fabricRef.id
);
const fabricObj = canvasObjects.find((obj) => obj.id === fabricRef.id);
if (fabricObj) {
fabricObj.layerId = layer.id;
fabricObj.layerName = layer.name;

View File

@@ -18,10 +18,8 @@ export async function restoreFabricObject(serializedObject, canvas) {
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId)
fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName)
fabricObject.layerName = serializedObject.layerName;
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
// 更新坐标
fabricObject.setCoords();

View File

@@ -22,11 +22,7 @@ export const createRasterizedImage = async ({
isGroupWithMask = false, // 是否为带遮罩的组图层
} = {}) => {
try {
console.log(
`📊 开始栅格化 ${fabricObjects.length} 个对象${
maskObject ? "(带遮罩)" : ""
}`
);
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象${maskObject ? "(带遮罩)" : ""}`);
// 确保有对象需要栅格化
if (fabricObjects.length === 0) {
@@ -36,10 +32,7 @@ export const createRasterizedImage = async ({
// 高清倍数
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom);
if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数
@@ -191,9 +184,7 @@ const createRasterizedImageWithGroup = async ({
hasControls: false,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 调整组的位置,让它位于画布的左上角
group.set({
@@ -317,9 +308,7 @@ const createRasterizedImageWithMask = async ({
height: canvasHeight,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 调整对象位置,相对于遮罩边界重新定位
clonedObjects.forEach((obj) => {

View File

@@ -85,18 +85,12 @@ const createClippedObjects = async ({
console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
const optimizedBounds = calculateOptimizedBounds(clippingObject, fabricObjects);
console.log("📐 优化后的选区边界框:", optimizedBounds);
// 获取羽化值
let featherAmount = 0;
if (
selectionManager &&
typeof selectionManager.getFeatherAmount === "function"
) {
if (selectionManager && typeof selectionManager.getFeatherAmount === "function") {
featherAmount = selectionManager.getFeatherAmount();
console.log(`🌟 应用羽化效果: ${featherAmount}px`);
}
@@ -177,10 +171,7 @@ const createClippedDataURLByCanvas = async ({
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
const optimizedBounds = calculateOptimizedBounds(clippingObject, fabricObjects);
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
@@ -239,13 +230,7 @@ const createClippedDataURLByCanvas = async ({
* 创建简单克隆对象
* 当不需要裁剪时,直接克隆原对象
*/
const createSimpleClone = async ({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
}) => {
const createSimpleClone = async ({ canvas, fabricObjects, isReturenDataURL, quality, format }) => {
try {
console.log("📋 创建简单克隆对象");
@@ -459,10 +444,7 @@ const createLegacyRasterization = async ({
// 这里保留原有的离屏渲染逻辑作为备选方案
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom);
scaleFactor = Math.min(scaleFactor, 3);
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
@@ -592,9 +574,7 @@ const createOffscreenRasterization = async ({
height: canvasHeight,
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`);
// 克隆对象到离屏画布
const clonedObjects = [];
@@ -749,11 +729,7 @@ export const getObjectsBounds = (fabricObjects) => {
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 遮罩图像的DataURL
*/
const createMaskImageFromPath = async ({
clippingObject,
selectionBounds,
qualityMultiplier,
}) => {
const createMaskImageFromPath = async ({ clippingObject, selectionBounds, qualityMultiplier }) => {
try {
console.log("🎭 创建路径遮罩图像");
@@ -769,11 +745,7 @@ const createMaskImageFromPath = async ({
});
// 克隆路径对象并处理描边转填充
const maskPath = await createSolidMaskPath(
clippingObject,
selectionBounds,
qualityMultiplier
);
const maskPath = await createSolidMaskPath(clippingObject, selectionBounds, qualityMultiplier);
// 添加路径到遮罩画布
maskCanvas.add(maskPath);
@@ -804,11 +776,7 @@ const createMaskImageFromPath = async ({
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 内容图像的DataURL
*/
const renderContentToImage = async ({
fabricObjects,
selectionBounds,
qualityMultiplier,
}) => {
const renderContentToImage = async ({ fabricObjects, selectionBounds, qualityMultiplier }) => {
try {
console.log("🖼️ 渲染内容图像");
@@ -964,11 +932,7 @@ const createAdvancedMaskImage = async ({
});
// 克隆路径对象并处理描边转填充
const maskPath = await createSolidMaskPath(
clippingObject,
selectionBounds,
qualityMultiplier
);
const maskPath = await createSolidMaskPath(clippingObject, selectionBounds, qualityMultiplier);
// 如果有羽化值,添加模糊效果
if (featherAmount > 0) {
@@ -987,10 +951,7 @@ const createAdvancedMaskImage = async ({
// 如果有羽化,需要进行后处理
if (featherAmount > 0) {
return await applyCanvasBlur(
maskCanvas,
featherAmount * qualityMultiplier
);
return await applyCanvasBlur(maskCanvas, featherAmount * qualityMultiplier);
}
// 生成遮罩图像
@@ -1066,11 +1027,7 @@ const applyCanvasBlur = async (canvas, blurAmount) => {
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<fabric.Object>} 处理后的遮罩路径对象
*/
const createSolidMaskPath = async (
clippingObject,
selectionBounds,
qualityMultiplier
) => {
const createSolidMaskPath = async (clippingObject, selectionBounds, qualityMultiplier) => {
try {
console.log("🔧 创建实体遮罩路径,处理描边转填充");
@@ -1081,29 +1038,19 @@ const createSolidMaskPath = async (
const hasStroke = maskPath.stroke && maskPath.strokeWidth > 0;
if (hasStroke) {
console.log(
`📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`
);
console.log(`📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`);
// 对于有描边的路径,我们需要更精确的处理
const strokeWidth = maskPath.strokeWidth;
// 方法1: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
if (
maskPath.type === "rect" ||
maskPath.type === "circle" ||
maskPath.type === "ellipse"
) {
if (maskPath.type === "rect" || maskPath.type === "circle" || maskPath.type === "ellipse") {
// 对于矩形和椭圆,增加宽高来包含描边
const strokeOffset = strokeWidth;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset / 2) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset / 2) *
qualityMultiplier,
left: (maskPath.left - selectionBounds.left - strokeOffset / 2) * qualityMultiplier,
top: (maskPath.top - selectionBounds.top - strokeOffset / 2) * qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
width: (maskPath.width || 0) + strokeOffset,
@@ -1122,12 +1069,8 @@ const createSolidMaskPath = async (
const strokeOffset = strokeWidth / 2;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset) *
qualityMultiplier,
left: (maskPath.left - selectionBounds.left - strokeOffset) * qualityMultiplier,
top: (maskPath.top - selectionBounds.top - strokeOffset) * qualityMultiplier,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier * expandRatio,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier * expandRatio,
fill: "#ffffff",