942 lines
24 KiB
JavaScript
942 lines
24 KiB
JavaScript
|
|
import { fabric } from "fabric-with-all";
|
|||
|
|
import { generateId } from "../utils/helper";
|
|||
|
|
import { OperationType } from "../utils/layerHelper";
|
|||
|
|
import { CreateSelectionCommand } from "../commands/SelectionCommands";
|
|||
|
|
import { ClearSelectionCommand } from "../commands/LassoCutoutCommand";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 部件选择管理器
|
|||
|
|
*/
|
|||
|
|
export class PartManager {
|
|||
|
|
/**
|
|||
|
|
* 创建部件选择管理器
|
|||
|
|
* @param {Object} options 配置选项
|
|||
|
|
* @param {Object} options.canvas fabric.js画布实例
|
|||
|
|
* @param {Object} options.commandManager 命令管理器实例
|
|||
|
|
* @param {Object} options.layerManager 图层管理实例
|
|||
|
|
*/
|
|||
|
|
constructor(options = {}) {
|
|||
|
|
this.canvas = options.canvas;
|
|||
|
|
this.commandManager = options.commandManager;
|
|||
|
|
this.layerManager = options.layerManager;
|
|||
|
|
|
|||
|
|
// 选区状态
|
|||
|
|
this.isActive = false;
|
|||
|
|
this.selectionType = OperationType.LASSO_RECTANGLE; // 使用常量而不是字符串
|
|||
|
|
this.selectionObject = null; // 当前选区对象
|
|||
|
|
this.selectionId = "selection_" + Date.now();
|
|||
|
|
this.featherAmount = 0; // 羽化值
|
|||
|
|
|
|||
|
|
// 选区样式配置
|
|||
|
|
this.selectionStyle = {
|
|||
|
|
stroke: "#0096ff",
|
|||
|
|
strokeWidth: 1,
|
|||
|
|
strokeDashArray: [5, 5],
|
|||
|
|
fill: "rgba(0, 150, 255, 0.1)",
|
|||
|
|
selectable: false,
|
|||
|
|
evented: false,
|
|||
|
|
excludeFromExport: true,
|
|||
|
|
hoverCursor: "default",
|
|||
|
|
moveCursor: "default",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 绘制状态
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
this.selectionPath = null; // 存储选区路径数据
|
|||
|
|
|
|||
|
|
// 自由选区相关状态
|
|||
|
|
this.drawingPoints = null;
|
|||
|
|
this.currentPathString = null;
|
|||
|
|
|
|||
|
|
// 不再直接绑定事件处理函数
|
|||
|
|
this._mouseDownHandler = null;
|
|||
|
|
this._mouseMoveHandler = null;
|
|||
|
|
this._mouseUpHandler = null;
|
|||
|
|
this._keyDownHandler = null;
|
|||
|
|
|
|||
|
|
// 选区相关的工具类型
|
|||
|
|
this.tools = [
|
|||
|
|
OperationType.PART,
|
|||
|
|
OperationType.PART_RECTANGLE,
|
|||
|
|
OperationType.PART_BRUSH,
|
|||
|
|
OperationType.PART_ERASER,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 当前工具
|
|||
|
|
this.currentTool = OperationType.SELECT;
|
|||
|
|
|
|||
|
|
// 选区状态变化回调
|
|||
|
|
this.onSelectionChanged = null;
|
|||
|
|
|
|||
|
|
// 不再自动初始化事件,改为手动控制
|
|||
|
|
// this.initEvents();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置当前工具
|
|||
|
|
* @param {String} toolId 工具ID
|
|||
|
|
*/
|
|||
|
|
setCurrentTool(toolId) {
|
|||
|
|
this.currentTool = toolId;
|
|||
|
|
|
|||
|
|
// 检查是否为选区工具
|
|||
|
|
const wasActive = this.isActive;
|
|||
|
|
this.isActive = this.tools.includes(toolId);
|
|||
|
|
|
|||
|
|
// 如果从非选区工具切换到选区工具,初始化事件
|
|||
|
|
if (!wasActive && this.isActive) {
|
|||
|
|
this.initEvents();
|
|||
|
|
}
|
|||
|
|
// 如果从选区工具切换到非选区工具,清理事件和选区
|
|||
|
|
else if (wasActive && !this.isActive) {
|
|||
|
|
this.cleanupEvents();
|
|||
|
|
this.clearSelection();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据工具类型设置选区类型
|
|||
|
|
if (this.isActive) {
|
|||
|
|
this.selectionType = toolId;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化选区相关事件
|
|||
|
|
*/
|
|||
|
|
initEvents() {
|
|||
|
|
if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化
|
|||
|
|
|
|||
|
|
// 保存实例引用,用于事件处理函数中
|
|||
|
|
const self = this;
|
|||
|
|
|
|||
|
|
// 鼠标按下事件处理
|
|||
|
|
this._mouseDownHandler = (options) => {
|
|||
|
|
// 如果选区功能未激活,不处理事件
|
|||
|
|
if (!this.isActive) return;
|
|||
|
|
|
|||
|
|
// 如果点击的是已有对象且不是选区对象,则不处理
|
|||
|
|
if (
|
|||
|
|
options.target &&
|
|||
|
|
options.target.id !== this.selectionId &&
|
|||
|
|
options.target.selectable !== false &&
|
|||
|
|
options.target.type !== "selection"
|
|||
|
|
) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 阻止事件冒泡,避免与 CanvasEventManager 冲突
|
|||
|
|
options.e.stopPropagation();
|
|||
|
|
|
|||
|
|
// 根据选区类型执行不同的起始操作
|
|||
|
|
switch (this.selectionType) {
|
|||
|
|
case OperationType.LASSO:
|
|||
|
|
this.startFreeSelection(options);
|
|||
|
|
break;
|
|||
|
|
case OperationType.LASSO_ELLIPSE:
|
|||
|
|
this.startEllipseSelection(options);
|
|||
|
|
break;
|
|||
|
|
case OperationType.LASSO_RECTANGLE:
|
|||
|
|
this.startRectangleSelection(options);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 鼠标移动事件处理
|
|||
|
|
this._mouseMoveHandler = (options) => {
|
|||
|
|
// 如果选区功能未激活或没有正在绘制的对象,不处理事件
|
|||
|
|
if (!this.isActive || !this.drawingObject) return;
|
|||
|
|
|
|||
|
|
// 阻止事件冒泡
|
|||
|
|
options.e.stopPropagation();
|
|||
|
|
|
|||
|
|
// 根据选区类型执行不同的绘制操作
|
|||
|
|
switch (this.selectionType) {
|
|||
|
|
case OperationType.LASSO_RECTANGLE:
|
|||
|
|
this.drawRectangleSelection(options);
|
|||
|
|
break;
|
|||
|
|
case OperationType.LASSO_ELLIPSE:
|
|||
|
|
this.drawEllipseSelection(options);
|
|||
|
|
break;
|
|||
|
|
case OperationType.LASSO:
|
|||
|
|
this.drawFreeSelection(options);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 鼠标抬起事件处理
|
|||
|
|
this._mouseUpHandler = (options) => {
|
|||
|
|
// 如果选区功能未激活或没有正在绘制的对象,不处理事件
|
|||
|
|
if (!this.isActive || !this.drawingObject) return;
|
|||
|
|
|
|||
|
|
// 阻止事件冒泡
|
|||
|
|
if (options && options.e) {
|
|||
|
|
options.e.stopPropagation();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据选区类型执行不同的完成操作
|
|||
|
|
switch (this.selectionType) {
|
|||
|
|
case OperationType.LASSO_RECTANGLE:
|
|||
|
|
this.endRectangleSelection();
|
|||
|
|
break;
|
|||
|
|
case OperationType.LASSO_ELLIPSE:
|
|||
|
|
this.endEllipseSelection();
|
|||
|
|
break;
|
|||
|
|
case OperationType.LASSO:
|
|||
|
|
this.endFreeSelection();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有命令管理器,使用命令模式记录选区创建
|
|||
|
|
if (this.commandManager && this.selectionObject) {
|
|||
|
|
this.commandManager.execute(
|
|||
|
|
new CreateSelectionCommand({
|
|||
|
|
canvas: this.canvas,
|
|||
|
|
selectionManager: this,
|
|||
|
|
selectionObject: this.selectionObject,
|
|||
|
|
selectionType: this.selectionType,
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 键盘事件处理
|
|||
|
|
this._keyDownHandler = (event) => {
|
|||
|
|
// 只在选区功能激活时处理键盘事件
|
|||
|
|
if (!this.isActive) return;
|
|||
|
|
|
|||
|
|
if (event.key === "Escape") {
|
|||
|
|
// ESC键取消当前选区操作
|
|||
|
|
if (this.drawingObject) {
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
}
|
|||
|
|
// 清除已有选区
|
|||
|
|
else if (this.selectionObject) {
|
|||
|
|
if (this.commandManager) {
|
|||
|
|
this.commandManager.execute(
|
|||
|
|
new ClearSelectionCommand({
|
|||
|
|
selectionManager: this,
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
this.clearSelection();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 添加事件监听
|
|||
|
|
this.canvas.on("mouse:down", this._mouseDownHandler);
|
|||
|
|
this.canvas.on("mouse:move", this._mouseMoveHandler);
|
|||
|
|
this.canvas.on("mouse:up", this._mouseUpHandler);
|
|||
|
|
|
|||
|
|
// 添加键盘事件监听
|
|||
|
|
document.addEventListener("keydown", this._keyDownHandler);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清理事件监听
|
|||
|
|
*/
|
|||
|
|
cleanupEvents() {
|
|||
|
|
if (!this.canvas) return;
|
|||
|
|
|
|||
|
|
// 移除事件监听
|
|||
|
|
if (this._mouseDownHandler) {
|
|||
|
|
this.canvas.off("mouse:down", this._mouseDownHandler);
|
|||
|
|
this._mouseDownHandler = null;
|
|||
|
|
}
|
|||
|
|
if (this._mouseMoveHandler) {
|
|||
|
|
this.canvas.off("mouse:move", this._mouseMoveHandler);
|
|||
|
|
this._mouseMoveHandler = null;
|
|||
|
|
}
|
|||
|
|
if (this._mouseUpHandler) {
|
|||
|
|
this.canvas.off("mouse:up", this._mouseUpHandler);
|
|||
|
|
this._mouseUpHandler = null;
|
|||
|
|
}
|
|||
|
|
if (this._keyDownHandler) {
|
|||
|
|
document.removeEventListener("keydown", this._keyDownHandler);
|
|||
|
|
this._keyDownHandler = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取选区对象
|
|||
|
|
* @returns {Object} 选区对象
|
|||
|
|
*/
|
|||
|
|
getSelectionObject() {
|
|||
|
|
return this.selectionObject;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取选区路径
|
|||
|
|
* @returns {Array|String} 选区路径数据
|
|||
|
|
*/
|
|||
|
|
getSelectionPath() {
|
|||
|
|
return this.selectionPath;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取羽化值
|
|||
|
|
* @returns {Number} 羽化值
|
|||
|
|
*/
|
|||
|
|
getFeatherAmount() {
|
|||
|
|
return this.featherAmount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置羽化值
|
|||
|
|
* @param {Number} amount 羽化值
|
|||
|
|
*/
|
|||
|
|
setFeatherAmount(amount) {
|
|||
|
|
this.featherAmount = amount;
|
|||
|
|
return this.updateSelectionAppearance();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置选区对象
|
|||
|
|
* @param {Object} object 选区对象
|
|||
|
|
*/
|
|||
|
|
setSelectionObject(object) {
|
|||
|
|
// 如果已存在选区,先移除
|
|||
|
|
if (this.selectionObject) {
|
|||
|
|
this.removeSelectionFromCanvas();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新选区对象
|
|||
|
|
this.selectionObject = object;
|
|||
|
|
this.selectionPath = object.path;
|
|||
|
|
this.selectionId = object.id || generateId();
|
|||
|
|
|
|||
|
|
// 更新外观
|
|||
|
|
this.updateSelectionAppearance();
|
|||
|
|
|
|||
|
|
// 添加到画布(确保在顶层)
|
|||
|
|
if (this.canvas && this.selectionObject) {
|
|||
|
|
this.canvas.add(this.selectionObject);
|
|||
|
|
this.canvas.bringToFront(this.selectionObject);
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 触发选区变化回调
|
|||
|
|
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
|
|||
|
|
this.onSelectionChanged();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从路径数据设置选区
|
|||
|
|
* @param {Array|String} path 选区路径数据
|
|||
|
|
*/
|
|||
|
|
setSelectionFromPath(path) {
|
|||
|
|
if (!path) return false;
|
|||
|
|
|
|||
|
|
// 创建选区对象
|
|||
|
|
const selectionObj = new fabric.Path(path, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 设置选区
|
|||
|
|
return this.setSelectionObject(selectionObj);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新选区外观
|
|||
|
|
*/
|
|||
|
|
updateSelectionAppearance() {
|
|||
|
|
if (!this.selectionObject) return false;
|
|||
|
|
|
|||
|
|
// 应用基本样式
|
|||
|
|
Object.assign(this.selectionObject, this.selectionStyle);
|
|||
|
|
|
|||
|
|
// 应用羽化效果
|
|||
|
|
if (this.featherAmount > 0) {
|
|||
|
|
this.selectionObject.shadow = new fabric.Shadow({
|
|||
|
|
color: "rgba(0, 150, 255, 0.5)",
|
|||
|
|
blur: this.featherAmount,
|
|||
|
|
offsetX: 0,
|
|||
|
|
offsetY: 0,
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
this.selectionObject.shadow = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新画布
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 移除选区
|
|||
|
|
*/
|
|||
|
|
removeSelectionFromCanvas() {
|
|||
|
|
if (this.canvas && this.selectionObject) {
|
|||
|
|
this.canvas.remove(this.selectionObject);
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清除选区
|
|||
|
|
*/
|
|||
|
|
clearSelection() {
|
|||
|
|
// 移除选区对象
|
|||
|
|
this.removeSelectionFromCanvas();
|
|||
|
|
|
|||
|
|
// 重置选区状态
|
|||
|
|
this.selectionObject = null;
|
|||
|
|
this.selectionPath = null;
|
|||
|
|
this.selectionId = null;
|
|||
|
|
this.featherAmount = 0;
|
|||
|
|
|
|||
|
|
// 触发选区变化回调
|
|||
|
|
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
|
|||
|
|
this.onSelectionChanged();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 反转选区
|
|||
|
|
*/
|
|||
|
|
async invertSelection() {
|
|||
|
|
if (!this.canvas || !this.selectionObject) return false;
|
|||
|
|
|
|||
|
|
// 获取画布范围
|
|||
|
|
const canvasRect = new fabric.Rect({
|
|||
|
|
left: 0,
|
|||
|
|
top: 0,
|
|||
|
|
width: this.canvas.width,
|
|||
|
|
height: this.canvas.height,
|
|||
|
|
selectable: false,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 创建反选路径
|
|||
|
|
let invertedPath;
|
|||
|
|
try {
|
|||
|
|
invertedPath = canvasRect.subtractPathFromRect(this.selectionObject.path);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("无法反转选区:", error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置新的选区
|
|||
|
|
const newSelection = new fabric.Path(invertedPath.path, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return this.setSelectionObject(newSelection);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加到选区
|
|||
|
|
* @param {Object} newSelection 要添加的选区对象
|
|||
|
|
*/
|
|||
|
|
async addToSelection(newSelection) {
|
|||
|
|
if (!this.canvas) return false;
|
|||
|
|
|
|||
|
|
// 如果当前没有选区,直接使用新选区
|
|||
|
|
if (!this.selectionObject) {
|
|||
|
|
return this.setSelectionObject(newSelection);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并选区
|
|||
|
|
let combinedPath;
|
|||
|
|
try {
|
|||
|
|
combinedPath = this.selectionObject.union(newSelection);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("无法添加到选区:", error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置新的选区
|
|||
|
|
const combinedSelection = new fabric.Path(combinedPath.path, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return this.setSelectionObject(combinedSelection);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从选区中移除
|
|||
|
|
* @param {Object} removeSelection 要移除的选区对象
|
|||
|
|
*/
|
|||
|
|
async removeFromSelection(removeSelection) {
|
|||
|
|
if (!this.canvas || !this.selectionObject) return false;
|
|||
|
|
|
|||
|
|
// 从当前选区中减去新选区
|
|||
|
|
let resultPath;
|
|||
|
|
try {
|
|||
|
|
resultPath = this.selectionObject.subtract(removeSelection);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("无法从选区中移除:", error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置新的选区
|
|||
|
|
const newSelection = new fabric.Path(resultPath.path, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return this.setSelectionObject(newSelection);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 应用羽化效果
|
|||
|
|
* @param {Number} amount 羽化值
|
|||
|
|
*/
|
|||
|
|
async featherSelection(amount) {
|
|||
|
|
if (!this.selectionObject) return false;
|
|||
|
|
|
|||
|
|
// 更新羽化值
|
|||
|
|
this.featherAmount = amount;
|
|||
|
|
|
|||
|
|
// 更新选区外观
|
|||
|
|
return this.updateSelectionAppearance();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查对象是否在选区内
|
|||
|
|
* @param {Object} object 要检查的对象
|
|||
|
|
* @returns {Boolean} 是否在选区内
|
|||
|
|
*/
|
|||
|
|
isObjectInSelection(object) {
|
|||
|
|
if (!this.selectionObject || !object) return false;
|
|||
|
|
|
|||
|
|
// 获取对象的边界框
|
|||
|
|
const bounds = object.getBoundingRect();
|
|||
|
|
const { left, top, width, height } = bounds;
|
|||
|
|
|
|||
|
|
// 检查对象的中心点和四个角是否在选区内
|
|||
|
|
const centerX = left + width / 2;
|
|||
|
|
const centerY = top + height / 2;
|
|||
|
|
|
|||
|
|
// 检查中心点
|
|||
|
|
if (this.isPointInSelection(centerX, centerY)) return true;
|
|||
|
|
|
|||
|
|
// 检查四个角
|
|||
|
|
if (this.isPointInSelection(left, top)) return true;
|
|||
|
|
if (this.isPointInSelection(left + width, top)) return true;
|
|||
|
|
if (this.isPointInSelection(left, top + height)) return true;
|
|||
|
|
if (this.isPointInSelection(left + width, top + height)) return true;
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查点是否在选区内
|
|||
|
|
* @param {Number} x X坐标
|
|||
|
|
* @param {Number} y Y坐标
|
|||
|
|
* @returns {Boolean} 是否在选区内
|
|||
|
|
*/
|
|||
|
|
isPointInSelection(x, y) {
|
|||
|
|
if (!this.selectionObject) return false;
|
|||
|
|
|
|||
|
|
// 使用fabric.js的containsPoint方法判断点是否在选区内
|
|||
|
|
return this.selectionObject.containsPoint({ x, y });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始自由选区
|
|||
|
|
* @param {Object} options 事件对象
|
|||
|
|
*/
|
|||
|
|
startFreeSelection(options) {
|
|||
|
|
if (!this.canvas || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取鼠标位置
|
|||
|
|
const pointer = this.canvas.getPointer(options.e);
|
|||
|
|
this.startPoint = pointer;
|
|||
|
|
|
|||
|
|
// 创建用于绘制轨迹的点数组
|
|||
|
|
this.drawingPoints = [pointer];
|
|||
|
|
|
|||
|
|
// 初始化SVG路径字符串
|
|||
|
|
this.currentPathString = `M ${pointer.x} ${pointer.y}`;
|
|||
|
|
|
|||
|
|
// 创建临时路径对象用于实时显示
|
|||
|
|
this.drawingObject = new fabric.Path(this.currentPathString, {
|
|||
|
|
stroke: this.selectionStyle.stroke,
|
|||
|
|
strokeWidth: this.selectionStyle.strokeWidth,
|
|||
|
|
strokeDashArray: this.selectionStyle.strokeDashArray,
|
|||
|
|
fill: "transparent",
|
|||
|
|
selectable: false,
|
|||
|
|
evented: false,
|
|||
|
|
strokeLineCap: "round",
|
|||
|
|
strokeLineJoin: "round",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 添加到画布
|
|||
|
|
this.canvas.add(this.drawingObject);
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 绘制自由选区
|
|||
|
|
* @param {Object} options 事件对象
|
|||
|
|
*/
|
|||
|
|
drawFreeSelection(options) {
|
|||
|
|
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取鼠标位置
|
|||
|
|
const pointer = this.canvas.getPointer(options.e);
|
|||
|
|
|
|||
|
|
// 添加新的点,但避免添加过于密集的点
|
|||
|
|
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
|
|||
|
|
const distance = Math.sqrt(
|
|||
|
|
Math.pow(pointer.x - lastPoint.x, 2) + Math.pow(pointer.y - lastPoint.y, 2)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 只有当距离大于2像素时才添加新点,避免路径过于复杂
|
|||
|
|
if (distance > 2) {
|
|||
|
|
this.drawingPoints.push(pointer);
|
|||
|
|
|
|||
|
|
// 更新路径字符串
|
|||
|
|
this.currentPathString += ` L ${pointer.x} ${pointer.y}`;
|
|||
|
|
|
|||
|
|
// 移除旧的绘制对象
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
|
|||
|
|
// 创建新的路径对象
|
|||
|
|
this.drawingObject = new fabric.Path(this.currentPathString, {
|
|||
|
|
stroke: this.selectionStyle.stroke,
|
|||
|
|
strokeWidth: this.selectionStyle.strokeWidth,
|
|||
|
|
strokeDashArray: this.selectionStyle.strokeDashArray,
|
|||
|
|
fill: "transparent",
|
|||
|
|
selectable: false,
|
|||
|
|
evented: false,
|
|||
|
|
strokeLineCap: "round",
|
|||
|
|
strokeLineJoin: "round",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 重新添加到画布
|
|||
|
|
this.canvas.add(this.drawingObject);
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 结束自由选区
|
|||
|
|
*/
|
|||
|
|
endFreeSelection() {
|
|||
|
|
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 检查是否有足够的点来形成选区
|
|||
|
|
if (this.drawingPoints.length < 3) {
|
|||
|
|
// 点太少,清除绘制对象
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.drawingPoints = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
this.currentPathString = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自动闭合路径 - 连接最后一点到第一点
|
|||
|
|
const firstPoint = this.drawingPoints[0];
|
|||
|
|
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
|
|||
|
|
const closingDistance = Math.sqrt(
|
|||
|
|
Math.pow(firstPoint.x - lastPoint.x, 2) + Math.pow(firstPoint.y - lastPoint.y, 2)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 如果首尾距离较大,自动添加闭合线段
|
|||
|
|
let finalPathString = this.currentPathString;
|
|||
|
|
if (closingDistance > 10) {
|
|||
|
|
finalPathString += ` L ${firstPoint.x} ${firstPoint.y}`;
|
|||
|
|
}
|
|||
|
|
finalPathString += " Z"; // 闭合路径
|
|||
|
|
|
|||
|
|
// 创建最终选区对象
|
|||
|
|
const selectionObj = new fabric.Path(finalPathString, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
fill: this.selectionStyle.fill, // 恢复填充
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 移除绘制中的临时对象
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
|
|||
|
|
// 重置绘制状态
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.drawingPoints = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
this.currentPathString = null;
|
|||
|
|
|
|||
|
|
// 设置选区
|
|||
|
|
this.setSelectionObject(selectionObj);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始矩形选区
|
|||
|
|
* @param {Object} options 事件对象
|
|||
|
|
*/
|
|||
|
|
startRectangleSelection(options) {
|
|||
|
|
if (!this.canvas || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取鼠标位置
|
|||
|
|
const pointer = this.canvas.getPointer(options.e);
|
|||
|
|
this.startPoint = pointer;
|
|||
|
|
|
|||
|
|
// 创建矩形对象
|
|||
|
|
this.drawingObject = new fabric.Rect({
|
|||
|
|
left: pointer.x,
|
|||
|
|
top: pointer.y,
|
|||
|
|
width: 0,
|
|||
|
|
height: 0,
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
fill: "transparent", // 在绘制过程中不显示填充
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 添加到画布
|
|||
|
|
this.canvas.add(this.drawingObject);
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 绘制矩形选区
|
|||
|
|
* @param {Object} options 事件对象
|
|||
|
|
*/
|
|||
|
|
drawRectangleSelection(options) {
|
|||
|
|
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取鼠标位置
|
|||
|
|
const pointer = this.canvas.getPointer(options.e);
|
|||
|
|
|
|||
|
|
// 计算宽度和高度
|
|||
|
|
const width = Math.abs(pointer.x - this.startPoint.x);
|
|||
|
|
const height = Math.abs(pointer.y - this.startPoint.y);
|
|||
|
|
|
|||
|
|
// 确定左上角坐标
|
|||
|
|
const left = Math.min(this.startPoint.x, pointer.x);
|
|||
|
|
const top = Math.min(this.startPoint.y, pointer.y);
|
|||
|
|
|
|||
|
|
// 更新矩形
|
|||
|
|
this.drawingObject.set({
|
|||
|
|
left: left,
|
|||
|
|
top: top,
|
|||
|
|
width: width,
|
|||
|
|
height: height,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 结束矩形选区
|
|||
|
|
*/
|
|||
|
|
endRectangleSelection() {
|
|||
|
|
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 将矩形转换为路径
|
|||
|
|
const left = this.drawingObject.left;
|
|||
|
|
const top = this.drawingObject.top;
|
|||
|
|
const width = this.drawingObject.width;
|
|||
|
|
const height = this.drawingObject.height;
|
|||
|
|
|
|||
|
|
// 如果矩形太小,忽略
|
|||
|
|
if (width < 5 || height < 5) {
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建矩形路径字符串
|
|||
|
|
const pathString = `M ${left} ${top} L ${left + width} ${top} L ${
|
|||
|
|
left + width
|
|||
|
|
} ${top + height} L ${left} ${top + height} Z`;
|
|||
|
|
|
|||
|
|
// 创建最终选区对象
|
|||
|
|
const selectionObj = new fabric.Path(pathString, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
fill: this.selectionStyle.fill, // 恢复填充
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 移除绘制中的临时对象
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
|
|||
|
|
// 重置绘制状态
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
|
|||
|
|
// 设置选区
|
|||
|
|
this.setSelectionObject(selectionObj);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始椭圆选区
|
|||
|
|
* @param {Object} options 事件对象
|
|||
|
|
*/
|
|||
|
|
startEllipseSelection(options) {
|
|||
|
|
if (!this.canvas || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取鼠标位置
|
|||
|
|
const pointer = this.canvas.getPointer(options.e);
|
|||
|
|
this.startPoint = pointer;
|
|||
|
|
|
|||
|
|
// 创建椭圆对象
|
|||
|
|
this.drawingObject = new fabric.Ellipse({
|
|||
|
|
left: pointer.x,
|
|||
|
|
top: pointer.y,
|
|||
|
|
rx: 0,
|
|||
|
|
ry: 0,
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
fill: "transparent", // 在绘制过程中不显示填充
|
|||
|
|
// originX: "left",
|
|||
|
|
// originY: "top",
|
|||
|
|
originX: "center",
|
|||
|
|
originY: "center",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 添加到画布
|
|||
|
|
this.canvas.add(this.drawingObject);
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 绘制椭圆选区
|
|||
|
|
* @param {Object} options 事件对象
|
|||
|
|
*/
|
|||
|
|
drawEllipseSelection(options) {
|
|||
|
|
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取鼠标位置
|
|||
|
|
const pointer = this.canvas.getPointer(options.e);
|
|||
|
|
|
|||
|
|
// 计算半径
|
|||
|
|
const rx = Math.abs(pointer.x - this.startPoint.x) / 2;
|
|||
|
|
const ry = Math.abs(pointer.y - this.startPoint.y) / 2;
|
|||
|
|
|
|||
|
|
// 确定中心坐标
|
|||
|
|
const left = Math.min(this.startPoint.x, pointer.x);
|
|||
|
|
const top = Math.min(this.startPoint.y, pointer.y);
|
|||
|
|
|
|||
|
|
// 更新椭圆
|
|||
|
|
this.drawingObject.set({
|
|||
|
|
left: left,
|
|||
|
|
top: top,
|
|||
|
|
rx: rx,
|
|||
|
|
ry: ry,
|
|||
|
|
originX: "left",
|
|||
|
|
originY: "top",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 结束椭圆选区
|
|||
|
|
*/
|
|||
|
|
endEllipseSelection() {
|
|||
|
|
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 获取椭圆参数
|
|||
|
|
const { left, top, rx, ry } = this.drawingObject;
|
|||
|
|
|
|||
|
|
// 如果椭圆太小,忽略
|
|||
|
|
if (rx < 2 || ry < 2) {
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算中心点
|
|||
|
|
const cx = left + rx;
|
|||
|
|
const cy = top + ry;
|
|||
|
|
|
|||
|
|
// 将椭圆转换为路径字符串
|
|||
|
|
const pathString = this.ellipseToSVGPath(cx, cy, rx, ry);
|
|||
|
|
|
|||
|
|
// 创建最终选区对象
|
|||
|
|
const selectionObj = new fabric.Path(pathString, {
|
|||
|
|
...this.selectionStyle,
|
|||
|
|
id: `selection_${Date.now()}`,
|
|||
|
|
name: "selection",
|
|||
|
|
fill: this.selectionStyle.fill, // 恢复填充
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 移除绘制中的临时对象
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
|
|||
|
|
// 重置绘制状态
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
|
|||
|
|
// 设置选区
|
|||
|
|
this.setSelectionObject(selectionObj);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将椭圆转换为SVG路径字符串
|
|||
|
|
* @param {Number} cx 中心点X坐标
|
|||
|
|
* @param {Number} cy 中心点Y坐标
|
|||
|
|
* @param {Number} rx X半径
|
|||
|
|
* @param {Number} ry Y半径
|
|||
|
|
* @returns {String} SVG路径字符串
|
|||
|
|
*/
|
|||
|
|
ellipseToSVGPath(cx, cy, rx, ry) {
|
|||
|
|
// 使用椭圆弧命令创建完整椭圆
|
|||
|
|
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${
|
|||
|
|
cx + rx
|
|||
|
|
} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置选区工具
|
|||
|
|
* @param {string} type 选区类型:OperationType.LASSO, OperationType.LASSO_RECTANGLE, OperationType.LASSO_ELLIPSE
|
|||
|
|
*/
|
|||
|
|
setSelectionType(type) {
|
|||
|
|
this.selectionType = type;
|
|||
|
|
|
|||
|
|
// 如果正在绘制,清除临时对象
|
|||
|
|
if (this.drawingObject) {
|
|||
|
|
this.canvas.remove(this.drawingObject);
|
|||
|
|
this.drawingObject = null;
|
|||
|
|
this.startPoint = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置选区工具的鼠标事件
|
|||
|
|
*/
|
|||
|
|
setupSelectionEvents() {
|
|||
|
|
// 选区事件现在通过 setCurrentTool 方法管理
|
|||
|
|
// 这个方法现在主要用于刷新或重置事件监听
|
|||
|
|
if (!this.canvas || !this.isActive) return;
|
|||
|
|
|
|||
|
|
// 确保选区处于激活状态
|
|||
|
|
if (this.tools.includes(this.currentTool)) {
|
|||
|
|
this.isActive = true;
|
|||
|
|
// 如果事件还没有初始化,初始化它们
|
|||
|
|
if (!this._mouseDownHandler) {
|
|||
|
|
this.initEvents();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清理资源
|
|||
|
|
*/
|
|||
|
|
dispose() {
|
|||
|
|
this.cleanupEvents();
|
|||
|
|
this.clearSelection();
|
|||
|
|
this.canvas = null;
|
|||
|
|
this.commandManager = null;
|
|||
|
|
this.layerManager = null;
|
|||
|
|
}
|
|||
|
|
}
|