接入画布
This commit is contained in:
@@ -0,0 +1,909 @@
|
||||
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.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) => {
|
||||
// 平滑停止任何正在进行的惯性动画
|
||||
this.stopInertiaAnimation(true);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置触摸事件处理
|
||||
*/
|
||||
setupTouchEvents() {
|
||||
// 触摸开始事件
|
||||
this.canvas.on("touch:gesture", (opt) => {
|
||||
// 平滑停止任何正在进行的惯性动画
|
||||
this.stopInertiaAnimation(true);
|
||||
|
||||
if (opt.e.touches && opt.e.touches.length === 2) {
|
||||
this.canvas.isDragging = true;
|
||||
this.canvas.lastPosX =
|
||||
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
|
||||
this.canvas.lastPosY =
|
||||
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
|
||||
|
||||
// 重置触摸位置历史
|
||||
this.dragStartTime = Date.now();
|
||||
this.lastMousePositions = [];
|
||||
|
||||
if (this.canvas.isDragging) {
|
||||
this.canvas.selection = false;
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
opt.e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// 单指触摸开始 - 处理拖动
|
||||
this.canvas.on("touch:drag", (opt) => {
|
||||
// 平滑停止任何正在进行的惯性动画
|
||||
this.stopInertiaAnimation(true);
|
||||
|
||||
if (this.editorMode === OperationType.PAN) {
|
||||
this.canvas.isDragging = true;
|
||||
this.canvas.lastPosX = opt.e.touches[0].clientX;
|
||||
this.canvas.lastPosY = opt.e.touches[0].clientY;
|
||||
|
||||
this.dragStartTime = Date.now();
|
||||
this.lastMousePositions = [];
|
||||
|
||||
if (this.canvas.isDragging) {
|
||||
this.canvas.selection = false;
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
opt.e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// 触摸移动事件
|
||||
this.canvas.on("touch:gesture:update", (opt) => {
|
||||
if (!this.canvas.isDragging) return;
|
||||
|
||||
if (opt.e.touches && opt.e.touches.length === 2) {
|
||||
const currentX =
|
||||
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
|
||||
const currentY =
|
||||
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
|
||||
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[4] += currentX - this.canvas.lastPosX;
|
||||
vpt[5] += currentY - this.canvas.lastPosY;
|
||||
|
||||
// 记录触摸位置和时间
|
||||
const now = Date.now();
|
||||
this.lastMousePositions.push({
|
||||
x: currentX,
|
||||
y: currentY,
|
||||
time: now,
|
||||
});
|
||||
|
||||
// 保持历史记录在限定数量内
|
||||
if (this.lastMousePositions.length > this.positionHistoryLimit) {
|
||||
this.lastMousePositions.shift();
|
||||
}
|
||||
|
||||
this.canvas.renderAll();
|
||||
this.canvas.lastPosX = currentX;
|
||||
this.canvas.lastPosY = currentY;
|
||||
opt.e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// 单指拖动更新
|
||||
this.canvas.on("touch:drag:update", (opt) => {
|
||||
if (!this.canvas.isDragging || this.editorMode !== OperationType.PAN)
|
||||
return;
|
||||
|
||||
const currentX = opt.e.touches[0].clientX;
|
||||
const currentY = opt.e.touches[0].clientY;
|
||||
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[4] += currentX - this.canvas.lastPosX;
|
||||
vpt[5] += currentY - this.canvas.lastPosY;
|
||||
|
||||
// 记录触摸位置和时间
|
||||
const now = Date.now();
|
||||
this.lastMousePositions.push({
|
||||
x: currentX,
|
||||
y: currentY,
|
||||
time: now,
|
||||
});
|
||||
|
||||
if (this.lastMousePositions.length > this.positionHistoryLimit) {
|
||||
this.lastMousePositions.shift();
|
||||
}
|
||||
|
||||
this.canvas.renderAll();
|
||||
this.canvas.lastPosX = currentX;
|
||||
this.canvas.lastPosY = currentY;
|
||||
opt.e.preventDefault();
|
||||
});
|
||||
|
||||
// 触摸结束事件
|
||||
this.canvas.on("touch:gesture:end", (opt) => {
|
||||
this.handleDragEnd(opt, true);
|
||||
});
|
||||
|
||||
// 单指拖动结束
|
||||
this.canvas.on("touch:drag:end", (opt) => {
|
||||
this.handleDragEnd(opt, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拖动结束(鼠标抬起或触摸结束)
|
||||
*/
|
||||
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,
|
||||
e.target
|
||||
);
|
||||
}, 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 || "对象"}`,
|
||||
});
|
||||
|
||||
// 执行并将命令添加到历史栈
|
||||
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) {
|
||||
this.thumbnailManager.clearElementThumbnail(e.target.id);
|
||||
|
||||
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
|
||||
if (e.target.parentId) {
|
||||
setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// // 鼠标抬起时,检查是否需要保存状态
|
||||
// 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 }) {
|
||||
// 确保有命令管理器
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行高保真合并操作
|
||||
try {
|
||||
console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
|
||||
|
||||
const command = await this.layerManager.LayerObjectsToGroup(
|
||||
activeLayer,
|
||||
fabricImage
|
||||
);
|
||||
|
||||
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.id === elementId ||
|
||||
(l.fabricObjects && l.fabricObjects?.[0]?.id === layerId)
|
||||
);
|
||||
|
||||
if (layer) {
|
||||
// 生成图层缩略图
|
||||
this.thumbnailManager.generateLayerThumbnail(layer);
|
||||
}
|
||||
|
||||
// 同时也维护元素缩略图,以保持向后兼容性
|
||||
this.thumbnailManager.generateElementThumbnail(
|
||||
{ id: elementId, type: fabricObject.type },
|
||||
fabricObject
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编辑器模式
|
||||
* @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);
|
||||
|
||||
// 检测设备类型
|
||||
const isMobile = /mobile|phone|android.*mobile|iphone/.test(userAgent);
|
||||
const isTablet = /tablet|ipad|android(?!.*mobile)/.test(userAgent);
|
||||
const isDesktop = !isMobile && !isTablet;
|
||||
|
||||
// 检测浏览器类型(用于特定优化)
|
||||
const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent);
|
||||
const isChrome = /chrome/.test(userAgent);
|
||||
const isFirefox = /firefox/.test(userAgent);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user