Files
aida_front/src/component/Canvas/CanvasEditor/managers/events/CanvasEventManager.js
2026-01-19 16:57:11 +08:00

1161 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isBoolean } from "lodash-es";
import { TransformCommand } from "../../commands/StateCommands";
import { generateId } from "../../utils/helper";
import { OperationType, OperationTypes } from "../../utils/layerHelper";
export class CanvasEventManager {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.toolManager = options.toolManager || null;
this.animationManager = options.animationManager;
this.thumbnailManager = options.thumbnailManager;
this.editorMode = options.editorMode || OperationType.SELECT;
this.activeElementId = options.activeElementId || { value: null };
this.layerManager = options.layerManager || null;
this.layers = options.layers || null;
this.lastSelectLayerId = options.lastSelectLayerId || null; // 最后选择的图层ID
// 事件处理的内部状态 - 优化设备检测
this.deviceInfo = this._detectDeviceType();
this.dragStartTime = 0;
this.lastMousePositions = [];
this.positionHistoryLimit = 5; // 追踪鼠标位置的历史记录,用于计算速度
this.longPressTimer = null;
this.longPressThreshold = 500;
// 初始化所有事件
this.initEvents();
}
initEvents() {
this.setupZoomEvents();
// 优化三端设备的事件处理逻辑
if (this.deviceInfo.isMobile || this.deviceInfo.isTablet) {
// 真正的移动设备和平板设备使用触摸事件
this.setupTouchEvents();
} else {
// PC 和 Mac 设备主要使用鼠标事件
this.setupMouseEvents();
}
// Mac 设备需要额外的触摸手势支持(用于特殊场景)
if (this.deviceInfo.isMac && this.deviceInfo.hasTouchSupport) {
this.setupMacTouchGestures();
}
// 共享事件
this.setupSelectionEvents();
this.setupObjectEvents();
this.setupDoubleClickEvents();
// this.setupHandlePathCreated();
}
setupZoomEvents() {
// 水平/垂直滚动相关状态
this._scrollWheelEvents = [];
this._scrollAccumulatedDelta = { x: 0, y: 0 };
this._scrollAccumulationTimeout = null;
this._scrollAccumulationTime = 100; // 降低滚轮累积时间窗口
this._lastScrollTime = 0; // 跟踪上次滚动时间
this._scrollThrottleDelay = 5; // 滚动节流延迟(毫秒)
// 缩放处理 - 使用动画管理器,针对 Mac 设备优化
this.canvas.on("mouse:wheel", (opt) => {
// Mac 设备双指滚动优化:确保滚动事件正确处理
if (this.deviceInfo.isMac) {
// Mac设备的简化处理逻辑减少不必要的动画中断
// 让动画管理器自行处理冲突,避免过度干预
} else {
// 非 Mac 设备的标准处理
if (
this.animationManager._panAnimation ||
this.animationManager._zoomAnimation
) {
this.animationManager._wasPanning =
!!this.animationManager._panAnimation;
this.animationManager._wasZooming =
!!this.animationManager._zoomAnimation;
this.animationManager.smoothStopAnimations({ duration: 0.1 });
}
}
// 按住 Ctrl 键时实现垂直滚动Mac 下是 Cmd 键)
const isCtrlOrCmd = this.deviceInfo.isMac ? opt.e.metaKey : opt.e.ctrlKey;
if (isCtrlOrCmd) {
this.handleScrollWheel(opt, "vertical");
opt.e.preventDefault();
return;
}
// 按住 Shift 键时实现水平滚动
if (opt.e.shiftKey) {
this.handleScrollWheel(opt, "horizontal");
opt.e.preventDefault();
return;
}
// 标准缩放行为 - 让 AnimationManager 处理平滑过渡
// Mac 设备下的双指滚动将直接进入这里进行缩放
this.animationManager.handleMouseWheel(opt);
});
}
/**
* 处理滚轮滚动事件
* @param {Object} opt 滚轮事件对象
* @param {String} direction 滚动方向: 'vertical' 或 'horizontal'
*/
handleScrollWheel(opt, direction) {
// 获取当前视图变换
const vpt = this.canvas.viewportTransform.slice(0); // 创建副本避免直接修改
const zoom = this.canvas.getZoom();
// 计算滚动量 - 根据方向决定是水平还是垂直滚动
let deltaX = 0;
let deltaY = 0;
// 设置滚动方向和距离
if (direction === "horizontal") {
deltaX = opt.e.deltaY; // 水平滚动
} else {
deltaY = opt.e.deltaY; // 垂直滚动
}
// 计算滚动因子,基于缩放级别和设备类型调整
let scrollFactor = Math.max(0.4, Math.min(1, 1 / zoom));
// Mac 设备优化:触控板滚动通常比鼠标滚轮更敏感
if (this.deviceInfo.isMac) {
const isMacTrackpadScroll =
Math.abs(opt.e.deltaY) < 100 && opt.e.deltaMode === 0;
if (isMacTrackpadScroll) {
// Mac 触控板滚动更细腻,需要调整滚动因子
scrollFactor *= 0.8; // 降低滚动敏感度
}
}
// 直接应用滚动变化,不使用累积和计时器
vpt[4] -= deltaX * scrollFactor;
vpt[5] -= deltaY * scrollFactor;
// 直接设置新的视图变换,不使用动画
this.canvas.setViewportTransform(vpt);
// 请求重新渲染画布
this.canvas.renderAll();
}
/**
* 处理累积的滚轮滚动事件并应用平移
* @private
* @param {String} direction 滚动方向
*/
_processAccumulatedScroll(direction) {
// 这个函数不再需要,但为了兼容性保留空实现
// 所有滚动逻辑已经移到 handleScrollWheel 中直接处理
return;
}
/**
* 停止所有惯性动画
* @param {boolean} smooth 是否平滑过渡,默认为 false立即停止
*/
stopInertiaAnimation(smooth = false) {
if (this.animationManager) {
if (this.animationManager._panAnimation && !smooth) {
this.animationManager._panAnimation.kill();
this.animationManager._panAnimation = null;
}
if (this.animationManager._zoomAnimation && !smooth) {
this.animationManager._zoomAnimation.kill();
this.animationManager._zoomAnimation = null;
}
}
}
/**
* 设置鼠标事件处理
*/
setupMouseEvents() {
// 鼠标按下事件
this.canvas.on("mouse:down", (opt) => {
// console.log("==========鼠标按下",opt)
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
// if (opt.e.which === 3 && this.editorMode === OperationType.SELECT) {
// console.log("==========选择模式鼠标右击画布对象")
// } else
if (
opt.e.altKey ||
opt.e.which === 2 ||
this.editorMode === OperationType.PAN
) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
this.canvas.defaultCursor = "grabbing";
// 记录拖动开始时间和位置,用于计算速度
this.dragStartTime = Date.now();
this.lastMousePositions = []; // 重置位置历史
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
}
});
// 鼠标移动事件
this.canvas.on("mouse:move", (opt) => {
if (!this.canvas.isDragging) return;
const vpt = this.canvas.viewportTransform;
vpt[4] += opt.e.clientX - this.canvas.lastPosX;
vpt[5] += opt.e.clientY - this.canvas.lastPosY;
// 记录鼠标位置和时间,用于计算惯性
const now = Date.now();
this.lastMousePositions.push({
x: opt.e.clientX,
y: opt.e.clientY,
time: now,
});
// 保持历史记录在限定数量内
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
});
// 鼠标抬起事件
this.canvas.on("mouse:up", (opt) => {
this.handleDragEnd(opt);
});
}
/**
* 设置触摸事件处理 - 修复iPad触摸事件支持
*/
setupTouchEvents() {
// 启用Fabric.js的指针事件支持适用于触摸设备
this.canvas.enablePointerEvents = true;
// 触摸状态管理
this.touchState = {
isZooming: false,
initialDistance: 0,
initialZoom: 1,
lastTouchTime: 0,
lastZoomTime: 0,
zoomThrottle: 16, // 约60fps的节流
};
// iPad特殊处理禁用默认的触摸行为
if (this.deviceInfo.isTablet) {
document.addEventListener(
"touchstart",
(e) => {
if (e.target.closest("canvas")) {
e.preventDefault();
}
},
{ passive: false }
);
document.addEventListener(
"touchmove",
(e) => {
if (e.target.closest("canvas")) {
e.preventDefault();
}
},
{ passive: false }
);
}
// 使用标准的mouse事件Fabric.js会自动处理触摸转换
this.canvas.on("mouse:down", (opt) => {
// 只在PAN模式下处理触摸事件
if (this.editorMode !== OperationType.PAN) {
return;
}
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
// 检查是否是触摸事件
const isTouch = opt.e.type && opt.e.type.includes("touch");
const touches =
opt.e.touches || (opt.e.originalEvent && opt.e.originalEvent.touches);
if (isTouch && touches && touches.length === 2) {
// 双指触摸 - 用于缩放
this.touchState.isZooming = true;
this.touchState.initialDistance = this.getTouchDistance(
touches[0],
touches[1]
);
this.touchState.initialZoom = this.canvas.getZoom();
// 计算缩放中心点
const centerX = (touches[0].clientX + touches[1].clientX) / 2;
const centerY = (touches[0].clientY + touches[1].clientY) / 2;
this.touchState.zoomCenter = { x: centerX, y: centerY };
opt.e.preventDefault();
} else if (isTouch && touches && touches.length === 1) {
// 单指触摸 - 用于拖拽
this.canvas.isDragging = true;
this.canvas.lastPosX = touches[0].clientX;
this.canvas.lastPosY = touches[0].clientY;
this.dragStartTime = Date.now();
this.lastMousePositions = [];
this.canvas.selection = false;
opt.e.preventDefault();
} else if (!isTouch) {
// 鼠标事件 - 用于拖拽
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
this.dragStartTime = Date.now();
this.lastMousePositions = [];
this.canvas.selection = false;
opt.e.preventDefault();
}
});
// 触摸移动事件 - 优化性能
this.canvas.on("mouse:move", (opt) => {
// 只在PAN模式下处理
if (this.editorMode !== OperationType.PAN) {
return;
}
// 检查是否是触摸事件
const isTouch = opt.e.type && opt.e.type.includes("touch");
const touches =
opt.e.touches || (opt.e.originalEvent && opt.e.originalEvent.touches);
if (
isTouch &&
touches &&
touches.length === 2 &&
this.touchState.isZooming
) {
// 双指缩放处理 - 修复抖动问题
const currentDistance = this.getTouchDistance(touches[0], touches[1]);
// 防止除零和异常值
if (this.touchState.initialDistance === 0 || currentDistance === 0) {
return;
}
const scale = currentDistance / this.touchState.initialDistance;
// 防止抖动:忽略微小的变化
if (Math.abs(scale - 1) < 0.01) {
return;
}
const newZoom = this.touchState.initialZoom * scale;
// 限制缩放范围
const clampedZoom = Math.max(0.1, Math.min(5, newZoom));
// 使用缩放中心点进行缩放
const point = new fabric.Point(
this.touchState.zoomCenter.x,
this.touchState.zoomCenter.y
);
this.canvas.zoomToPoint(point, clampedZoom);
opt.e.preventDefault();
return;
}
if (!this.canvas.isDragging) return;
let currentX, currentY;
if (isTouch && touches && touches.length === 1) {
// 单指触摸移动
currentX = touches[0].clientX;
currentY = touches[0].clientY;
} else if (!isTouch) {
// 鼠标移动
currentX = opt.e.clientX;
currentY = opt.e.clientY;
} else {
return; // 忽略其他情况
}
// 优化减少频繁的DOM操作
const deltaX = currentX - this.canvas.lastPosX;
const deltaY = currentY - this.canvas.lastPosY;
// 只有移动距离足够大时才更新
if (Math.abs(deltaX) < 1 && Math.abs(deltaY) < 1) {
return;
}
const vpt = this.canvas.viewportTransform;
vpt[4] += deltaX;
vpt[5] += deltaY;
// 优化:减少历史记录频率
const now = Date.now();
if (now - this.touchState.lastTouchTime > 16) {
// 约60fps
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.touchState.lastTouchTime = now;
}
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault();
});
// 触摸结束事件
this.canvas.on("mouse:up", (opt) => {
// 只在PAN模式下处理
if (this.editorMode !== OperationType.PAN) {
return;
}
// 重置触摸状态
this.touchState.isZooming = false;
this.touchState.initialDistance = 0;
this.handleDragEnd(opt, true);
});
// 添加原生触摸事件监听器作为备用方案
this.setupNativeTouchEvents();
}
/**
* 计算两个触摸点之间的距离
*/
getTouchDistance(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 设置原生触摸事件监听器(备用方案)- 专门处理iPad双指缩放
*/
setupNativeTouchEvents() {
const canvasElement = this.canvas.upperCanvasEl;
// 确保canvas元素支持触摸
canvasElement.style.touchAction = "none";
let lastTouchDistance = 0;
let lastZoom = 1;
// 原生touchstart事件 - 处理双指缩放初始化
canvasElement.addEventListener(
"touchstart",
(e) => {
if (this.editorMode !== OperationType.PAN) return;
// 调试信息
if (process.env.NODE_ENV === "development") {
// console.log("iPad touchstart:", e.touches.length, "fingers");
}
if (e.touches.length === 2) {
// 双指触摸开始
this.touchState.isZooming = true;
lastTouchDistance = this.getTouchDistance(e.touches[0], e.touches[1]);
lastZoom = this.canvas.getZoom();
// 计算缩放中心点
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
this.touchState.zoomCenter = { x: centerX, y: centerY };
if (process.env.NODE_ENV === "development") {
// console.log("iPad双指缩放开始:", {
// distance: lastTouchDistance,
// zoom: lastZoom,
// center: this.touchState.zoomCenter,
// });
}
e.preventDefault();
}
},
{ passive: false }
);
// 原生touchmove事件 - 处理双指缩放(修复抖动问题)
canvasElement.addEventListener(
"touchmove",
(e) => {
if (this.editorMode !== OperationType.PAN) return;
if (e.touches.length === 2 && this.touchState.isZooming) {
// 节流处理,避免过于频繁的缩放操作
const now = Date.now();
if (
now - this.touchState.lastZoomTime <
this.touchState.zoomThrottle
) {
return;
}
const currentDistance = this.getTouchDistance(
e.touches[0],
e.touches[1]
);
// 防止除零和异常值
if (lastTouchDistance === 0 || currentDistance === 0) {
return;
}
const scale = currentDistance / lastTouchDistance;
// 防止抖动:忽略微小的变化
if (Math.abs(scale - 1) < 0.02) {
return;
}
// 使用当前缩放值而不是初始缩放值,避免累积误差
const currentZoom = this.canvas.getZoom();
const newZoom = currentZoom * scale;
// 限制缩放范围
const clampedZoom = Math.max(0.1, Math.min(5, newZoom));
if (process.env.NODE_ENV === "development") {
// console.log("iPad双指缩放中:", {
// currentDistance,
// lastTouchDistance,
// scale,
// currentZoom,
// newZoom,
// clampedZoom,
// });
}
// 使用缩放中心点进行缩放
const point = new fabric.Point(
this.touchState.zoomCenter.x,
this.touchState.zoomCenter.y
);
this.canvas.zoomToPoint(point, clampedZoom);
// 更新基准距离和时间,避免累积误差
lastTouchDistance = currentDistance;
this.touchState.lastZoomTime = now;
e.preventDefault();
}
},
{ passive: false }
);
// 原生touchend事件 - 重置缩放状态
canvasElement.addEventListener(
"touchend",
(e) => {
if (this.editorMode !== OperationType.PAN) return;
if (e.touches.length < 2) {
this.touchState.isZooming = false;
lastTouchDistance = 0;
}
e.preventDefault();
},
{ passive: false }
);
}
/**
* 处理拖动结束(鼠标抬起或触摸结束)
*/
handleDragEnd(opt, isTouch = false) {
if (this.canvas.isDragging) {
// 使用动画管理器处理惯性效果
if (this.lastMousePositions.length > 1 && opt && opt.e) {
this.animationManager.applyInertiaEffect(
this.lastMousePositions,
isTouch
);
}
}
this.canvas.isDragging = false;
if (this.toolManager) {
this.toolManager.restoreSelectionState(); // 恢复选择状态
}
this.canvas.renderAll();
}
setupSelectionEvents() {
// 监听对象选择事件
this.canvas.on("selection:created", (opt) => this.updateSelectedLayer(opt));
this.canvas.on("selection:updated", (opt) => this.updateSelectedLayer(opt));
// this.canvas.on("selection:cleared", () => this.clearSelectedElements());
}
setupObjectEvents() {
// 监听对象变化事件,用于更新缩略图
this.canvas.on("object:added", (e) => {
if (this.thumbnailManager && e.target && e.target.id) {
// 延迟更新以确保对象完全添加
setTimeout(() => {
// 现在图层就是元素本身,直接更新元素的缩略图
this.thumbnailManager.generateLayerThumbnail(e.target.layerId);
}, 300);
}
});
// 添加对象开始变换时的状态捕获
this.canvas.on(
"object:moving",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:scaling",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:rotating",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:skewing",
this._captureInitialTransformState.bind(this)
);
this.canvas.on("object:modified", (e) => {
// 移除调试日志
// console.log("object:modified", e);
const activeObj = e.target || this.canvas.getActiveObject();
if (activeObj && this.layerManager?.commandManager) {
// 使用新的轻量级 TransformCommand 替代完整状态保存
// 检查对象是否有初始变换状态记录
if (activeObj._initialTransformState) {
// 创建并执行 TransformCommand只记录变换属性的变化
const transformCmd = new TransformCommand({
canvas: this.canvas,
objectId: activeObj.id,
initialState: activeObj._initialTransformState,
finalState: TransformCommand.captureTransformState(activeObj),
objectType: activeObj.type,
name: `变换 ${activeObj.type || "对象"}`,
layerManager: this.layerManager,
layers: this.layers,
lastSelectLayerId: this.lastSelectLayerId,
});
// 执行并将命令添加到历史栈
this.layerManager.commandManager.execute(transformCmd, {
name: "对象修改",
});
// 清除临时状态记录
delete activeObj._initialTransformState;
}
}
if (this.thumbnailManager && e.target) {
if (e.target.id) {
this.updateLayerThumbnail(e.target.id, e.target);
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
this.updateLayerThumbnail(e.target.parentId);
}
}
}
});
this.canvas.on("object:removed", (e) => {
if (this.thumbnailManager && e.target) {
if (e.target.id) {
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
// setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50);
this.thumbnailManager.generateLayerThumbnail(e.target.parentId);
}
}
}
});
// // 鼠标抬起时,检查是否需要保存状态
// this.canvas.on("mouse:up", (e) => {
// // 只在选择模式下处理对象变换的状态保存
// if (this.editorMode !== OperationType.SELECT) {
// // 绘画、擦除等模式通过各自的命令管理状态,不需要在这里保存
// return;
// }
// const activeObj = this.canvas.getActiveObject();
// if (
// activeObj &&
// activeObj._stateRecord &&
// activeObj._stateRecord.isModifying
// ) {
// const original = activeObj._stateRecord.originalState;
// // 检查是否是真正的变换操作(移动、缩放、旋转)
// const hasTransformChanged =
// original.left !== activeObj.left ||
// original.top !== activeObj.top ||
// original.scaleX !== activeObj.scaleX ||
// original.scaleY !== activeObj.scaleY ||
// original.angle !== activeObj.angle;
// // 只有在对象发生变换且不是命令执行过程中时才保存状态
// if (hasTransformChanged && this.layerManager) {
// // 立即保存状态,而不是延迟执行
// this.layerManager.saveCanvasState();
// delete activeObj._stateRecord;
// } else {
// // 清理状态记录,即使没有保存状态
// delete activeObj._stateRecord;
// }
// }
// });
}
setupDoubleClickEvents() {
// 双击处理
this.canvas.on("mouse:dblclick", (opt) => {
if (opt.target) {
// 双击对象的特殊处理
} else {
// 双击空白处重置缩放
if (this.animationManager) {
this.animationManager.resetZoom(true);
}
}
});
}
setupLongPress(callback) {
this.canvas.on("mouse:down", (opt) => {
if (!opt.target) return;
this.longPressTimer = setTimeout(() => {
callback(opt);
}, this.longPressThreshold);
});
this.canvas.on("mouse:up", () => {
clearTimeout(this.longPressTimer);
});
this.canvas.on("mouse:move", () => {
clearTimeout(this.longPressTimer);
});
}
// 设置路径创建事件
setupHandlePathCreated() {
// 在 CanvasEventManager 的构造函数或初始化方法中
// this.canvas.on("path:created", this._handlePathCreated.bind(this));
}
_handlePathCreated(e) {
// // 获取新创建的路径对象
// const path = e.path;
// // 设置路径的ID和其他属性
// path.id = generateId(); // 生成唯一ID
// // 获取当前活动图层
// const activeLayer = this.layerManager.getActiveLayer();
// // 将路径对象绑定到当前活动图层
// if (activeLayer) {
// // 设置路径的图层ID
// path.layerId = activeLayer.id;
// // 更新图层对象列表
// if (!activeLayer.fabricObjects) activeLayer.fabricObjects = [];
// activeLayer.fabricObjects.push(path);
// // 更新图层缩略图
// if (this.thumbnailManager) {
// this.thumbnailManager.generateLayerThumbnail(activeLayer.id);
// }
// }
}
/**
* 合并图层中的对象为组以提高性能
* @param {Object} options 合并选项
* @param {fabric.Image} options.fabricImage 新的图像对象
* @param {Object} options.activeLayer 当前活动图层
* @private
*/
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer, options }) {
// 确保有命令管理器
if (!this.layerManager || !this.layerManager.commandManager) {
console.warn("合并对象失败:没有命令管理器");
return;
}
// 确保有活动图层
if (!activeLayer) {
console.warn("合并对象失败:没有活动图层");
return;
}
// 验证是否需要合并
const hasExistingObjects =
Array.isArray(activeLayer.fabricObjects) &&
activeLayer.fabricObjects.length > 0;
const hasNewImage = !!fabricImage;
if (!hasExistingObjects && !hasNewImage) {
// console.log("没有对象需要合并");
return;
}
// 如果只有一个新图像且图层为空,直接添加到图层
if (hasNewImage && !hasExistingObjects) {
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id, options);
return;
}
// 执行高保真合并操作
try {
// console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
const command = await this.layerManager.LayerObjectsToGroup(
activeLayer,
fabricImage
);
// 设置命令的撤销状态
if (isBoolean(options.undoable)) command.undoable = options.undoable; // 是否撤销
this.layerManager?.commandManager?.execute?.(command, {
name: `合并图层 ${activeLayer.name} 中的对象为组`,
});
} catch (error) {
console.error("合并图层对象时发生错误:", error);
// 降级处理:如果合并失败,至少保证新图像能添加到图层
if (fabricImage && this.layerManager) {
// console.log("执行降级处理:直接添加图像到图层");
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
}
}
}
updateSelectedLayer(opt) {
const selected = opt.selected[0];
if (selected) {
this.layerManager.activeLayerId.value = selected.layerId;
}
}
// clearSelectedElements() {
// this.activeElementId.value = null;
// }
// 更新图层缩略图
updateLayerThumbnail(layerId) {
if (!this.thumbnailManager || !layerId || !this.layers) return;
const layer = this.layers.value.find((l) => l.id === layerId);
if (layer) {
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
// 更新子元素组合缩略图
updateLayerChidrenThumbnail(layerId, fabricObject) {
if (!this.thumbnailManager || !fabricObject || !this.layers) return;
// 查找对应的图层(现在元素就是图层)
const layer = this.layers.value.find(
(l) => l.fabricObjects && l.fabricObjects?.[0]?.id === layerId
);
if (layer) {
// 生成图层缩略图
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
/**
* 设置编辑器模式
* @param {string} mode 编辑器模式
*/
setEditorMode(mode) {
if (!OperationTypes.includes(mode)) {
console.warn(`不支持的编辑器模式: ${mode}`);
return;
}
// 切换工具时,立即停止任何惯性动画,但使用平滑过渡
this.stopInertiaAnimation(true);
this.editorMode = mode;
// 如果切换到选择模式,还原鼠标指针
if (mode === OperationType.SELECT) {
this.canvas.defaultCursor = "default";
} else if (mode === OperationType.PAN) {
this.canvas.defaultCursor = "grab";
}
}
dispose() {
// 移除所有事件监听
this.canvas.off();
// 清理 Mac 专用的原生事件监听器
if (this.deviceInfo.isMac && this.canvas.upperCanvasEl) {
const upperCanvas = this.canvas.upperCanvasEl;
// 移除手势事件监听器
upperCanvas.removeEventListener("gesturestart", null);
upperCanvas.removeEventListener("gesturechange", null);
upperCanvas.removeEventListener("gestureend", null);
upperCanvas.removeEventListener("wheel", null);
}
// 清除计时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// 停止所有动画
this.stopInertiaAnimation();
}
/**
* 捕获对象开始变换时的初始状态
* @private
* @param {Object} e 事件对象
*/
_captureInitialTransformState(e) {
const obj = e.target;
// 只在首次触发变换事件时记录初始状态
if (obj && !obj._initialTransformState && obj.id) {
// 捕获对象的初始变换状态
obj._initialTransformState = TransformCommand.captureTransformState(obj);
// 添加调试日志(可选)
// console.log(`捕获对象 ${obj.id} (${obj.type}) 的初始变换状态`);
}
}
/**
* 精确检测设备类型,区分 PC、Mac、平板和移动设备
* @private
* @returns {Object} 设备信息对象
*/
_detectDeviceType() {
const userAgent = navigator.userAgent.toLowerCase();
const platform = navigator.platform.toLowerCase();
const hasTouchSupport =
"ontouchstart" in window || navigator.maxTouchPoints > 0;
// 检测操作系统
const isMac = /mac|darwin/.test(platform) || /macintosh/.test(userAgent);
const isWindows = /win/.test(platform);
const isLinux = /linux/.test(platform) && !/android/.test(userAgent);
// 检测设备类型 - 修复iPad检测逻辑
const isMobile = /mobile|phone|android.*mobile|iphone/.test(userAgent);
// 修复iPad检测包括iOS iPad和Android平板
const isTablet =
/tablet|ipad|android(?!.*mobile)/.test(userAgent) ||
/ipad/.test(userAgent) ||
(navigator.maxTouchPoints &&
navigator.maxTouchPoints > 1 &&
/mac/.test(userAgent));
const isDesktop = !isMobile && !isTablet;
// 检测浏览器类型(用于特定优化)
const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent);
const isChrome = /chrome/.test(userAgent);
const isFirefox = /firefox/.test(userAgent);
// 调试日志 - 仅在开发环境输出
if (process.env.NODE_ENV === "development") {
// console.log("设备检测结果:", {
// userAgent,
// platform,
// isMobile,
// isTablet,
// isDesktop,
// hasTouchSupport,
// maxTouchPoints: navigator.maxTouchPoints,
// });
}
return {
isMac,
isWindows,
isLinux,
isMobile,
isTablet,
isDesktop,
isSafari,
isChrome,
isFirefox,
hasTouchSupport,
// 判断是否应该使用触摸事件作为主要交互方式
preferTouchEvents: (isMobile || isTablet) && !isDesktop,
// 判断是否需要特殊的 Mac 触控板处理
needsMacTrackpadOptimization: isMac && isDesktop && hasTouchSupport,
};
}
/**
* 设置 Mac 专用的触摸手势处理
* 主要用于处理触控板的多指手势,但不干扰双指滚动的缩放功能
*/
setupMacTouchGestures() {
// Mac 触控板专用:三指拖拽进行画布平移
let macGestureState = {
isThreeFingerDrag: false,
startX: 0,
startY: 0,
};
// 监听 Mac 专用的手势事件
this.canvas.upperCanvasEl.addEventListener(
"gesturestart",
(e) => {
// 阻止浏览器默认的手势行为,但保留双指缩放
if (e.scale !== 1) {
e.preventDefault();
}
},
{ passive: false }
);
this.canvas.upperCanvasEl.addEventListener(
"gesturechange",
(e) => {
// 只处理三指以上的手势,保留双指缩放给 mouse:wheel 事件
if (e.touches && e.touches.length >= 3) {
e.preventDefault();
if (!macGestureState.isThreeFingerDrag) {
macGestureState.isThreeFingerDrag = true;
macGestureState.startX = e.pageX;
macGestureState.startY = e.pageY;
this.canvas.isDragging = true;
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;
this.stopInertiaAnimation(true);
} else {
// 执行三指拖拽平移
const vpt = this.canvas.viewportTransform;
vpt[4] += e.pageX - this.canvas.lastPosX;
vpt[5] += e.pageY - this.canvas.lastPosY;
this.canvas.renderAll();
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;
}
}
},
{ passive: false }
);
this.canvas.upperCanvasEl.addEventListener(
"gestureend",
(e) => {
if (macGestureState.isThreeFingerDrag) {
macGestureState.isThreeFingerDrag = false;
this.canvas.isDragging = false;
if (this.toolManager) {
this.toolManager.restoreSelectionState();
}
this.canvas.renderAll();
}
},
{ passive: false }
);
// 添加 Mac 专用的鼠标滚轮优化,确保双指滚动正常工作
this.setupMacScrollOptimization();
}
/**
* Mac 滚轮优化:确保双指滚动正确触发缩放
*/
setupMacScrollOptimization() {
if (!this.deviceInfo.isMac) return;
// Mac 下的滚轮事件优化
let macScrollState = {
lastWheelTime: 0,
wheelTimeout: null,
};
// 监听原生滚轮事件,确保 Mac 双指滚动正确处理
this.canvas.upperCanvasEl.addEventListener(
"wheel",
(e) => {
const now = Date.now();
// Mac 双指滚动的特征:通常有较高的 deltaY 精度和连续性
const isMacTrackpadScroll =
this.deviceInfo.isMac &&
Math.abs(e.deltaY) < 100 && // 像素模式
e.deltaMode === 0; // 像素模式
if (isMacTrackpadScroll) {
// 清除之前的超时
if (macScrollState.wheelTimeout) {
clearTimeout(macScrollState.wheelTimeout);
}
// 确保这个事件会被 Fabric.js 的 mouse:wheel 正确处理
macScrollState.lastWheelTime = now;
// 设置短暂延迟,防止与触摸事件冲突
macScrollState.wheelTimeout = setTimeout(() => {
// 滚轮事件处理完成
}, 16); // 约一帧的时间
}
},
{ passive: true }
);
}
}