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

@@ -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;
}
}
};