2026-03-13 11:18:36 +08:00
|
|
|
import { fabric } from 'fabric-with-all'
|
2026-03-25 11:27:24 +08:00
|
|
|
import { OperationType } from '../tools/layerHelper'
|
|
|
|
|
import { getObjectAlphaToCanvas, traceImageContour, cloneObjects } from '../tools/canvasMethod'
|
2026-03-20 13:23:00 +08:00
|
|
|
|
2026-03-13 11:18:36 +08:00
|
|
|
/** 智能框选工具管理器 */
|
|
|
|
|
export class AISelectboxToolManager {
|
|
|
|
|
// 管理器
|
|
|
|
|
canvasManager: any
|
|
|
|
|
stateManager: any
|
|
|
|
|
layerManager: any
|
2026-03-23 16:43:08 +08:00
|
|
|
toolManager: any
|
2026-03-13 11:18:36 +08:00
|
|
|
|
|
|
|
|
isDragging: boolean = false
|
|
|
|
|
startX: number = 0
|
|
|
|
|
startY: number = 0
|
2026-03-25 11:27:24 +08:00
|
|
|
|
|
|
|
|
indicatorObject: any// 指示框对象
|
|
|
|
|
demoObject: any// 演示框对象
|
|
|
|
|
tcanvas: any// 临时画布对象
|
|
|
|
|
|
|
|
|
|
tools = [
|
|
|
|
|
OperationType.AISELECT_ADD,
|
|
|
|
|
OperationType.AISELECT_REMOVE,
|
|
|
|
|
OperationType.AISELECT_DRAW,
|
|
|
|
|
OperationType.AISELECT_ERASER
|
|
|
|
|
]
|
2026-03-13 11:18:36 +08:00
|
|
|
constructor(options) {
|
|
|
|
|
this.canvasManager = options.canvasManager
|
|
|
|
|
this.stateManager = options.stateManager
|
|
|
|
|
this.layerManager = options.layerManager
|
2026-03-23 16:43:08 +08:00
|
|
|
this.toolManager = options.toolManager
|
2026-03-13 11:18:36 +08:00
|
|
|
}
|
2026-03-24 11:49:53 +08:00
|
|
|
/** 处理切换工具 */
|
|
|
|
|
handleToolChange(oldTool: string, newTool: string) {
|
2026-03-25 11:27:24 +08:00
|
|
|
const oldIsAAA = this.tools.includes(oldTool)
|
|
|
|
|
const newIsAAA = this.tools.includes(newTool)
|
|
|
|
|
if (!oldIsAAA && newIsAAA) {
|
|
|
|
|
// 普通工具切换到智能框选工具
|
|
|
|
|
this.init()
|
|
|
|
|
} else if (oldIsAAA && !newIsAAA) {
|
|
|
|
|
// 智能框选工具切换到普通工具
|
|
|
|
|
this.clear()
|
2026-03-24 11:49:53 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-25 11:27:24 +08:00
|
|
|
/** 切换到橡皮擦工具 */
|
|
|
|
|
changeToolToEraser() {
|
|
|
|
|
if (!this.demoObject) return;
|
|
|
|
|
this.demoObject.set({ erasable: true })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
console.log("初始化智能框选工具")
|
|
|
|
|
this.clear();
|
|
|
|
|
this.createDemoObject()
|
|
|
|
|
this.tcanvas = null;
|
|
|
|
|
}
|
|
|
|
|
clear() {
|
|
|
|
|
console.log("清除智能框选工具")
|
|
|
|
|
this.clearDemoObject()
|
|
|
|
|
this.clearIndicatorObject()
|
|
|
|
|
this.isDragging = false
|
|
|
|
|
this.canvasManager.canvas.renderAll()
|
|
|
|
|
}
|
2026-03-25 14:37:20 +08:00
|
|
|
demoObjectDefault = {
|
|
|
|
|
left: 0,
|
|
|
|
|
top: 0,
|
|
|
|
|
evented: false,
|
|
|
|
|
selectable: false,
|
|
|
|
|
erasable: false,
|
|
|
|
|
}
|
2026-03-25 11:27:24 +08:00
|
|
|
createDemoObject() {
|
|
|
|
|
if (this.demoObject) this.clearDemoObject()
|
|
|
|
|
const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
|
|
|
|
|
const canvas = document.createElement('canvas')
|
|
|
|
|
canvas.width = canvasWidth
|
|
|
|
|
canvas.height = canvasHeight
|
2026-03-25 14:37:20 +08:00
|
|
|
this.demoObject = new fabric.Image(canvas, this.demoObjectDefault)
|
2026-03-25 11:27:24 +08:00
|
|
|
this.canvasManager.canvas.add(this.demoObject)
|
|
|
|
|
this.canvasManager.canvas.renderAll()
|
|
|
|
|
}
|
|
|
|
|
clearDemoObject() {
|
|
|
|
|
this.canvasManager.canvas.remove(this.demoObject)
|
|
|
|
|
this.demoObject = null
|
|
|
|
|
}
|
|
|
|
|
createIndicatorObject() {
|
2026-03-13 11:18:36 +08:00
|
|
|
const rect = new fabric.Rect({
|
|
|
|
|
left: this.startX,
|
|
|
|
|
top: this.startY,
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0,
|
|
|
|
|
fill: 'transparent',
|
|
|
|
|
stroke: '#000',
|
|
|
|
|
strokeWidth: 1,
|
|
|
|
|
evented: false,
|
|
|
|
|
})
|
2026-03-25 11:27:24 +08:00
|
|
|
this.indicatorObject = rect
|
2026-03-13 11:18:36 +08:00
|
|
|
this.canvasManager.canvas.add(rect)
|
|
|
|
|
this.canvasManager.canvas.renderAll()
|
|
|
|
|
}
|
2026-03-25 11:27:24 +08:00
|
|
|
clearIndicatorObject() {
|
|
|
|
|
this.canvasManager.canvas.remove(this.indicatorObject)
|
|
|
|
|
this.indicatorObject = null
|
|
|
|
|
}
|
2026-03-25 14:37:20 +08:00
|
|
|
// 创建临时画布对象
|
|
|
|
|
async createStaticCanvas(object: fabric.Object) {
|
|
|
|
|
if (!this.demoObject) this.createDemoObject()
|
|
|
|
|
const canvas = new fabric.StaticCanvas(document.createElement("canvas"), {
|
2026-03-25 11:27:24 +08:00
|
|
|
width: this.demoObject.width,
|
|
|
|
|
height: this.demoObject.height,
|
|
|
|
|
enableRetinaScaling: false,
|
|
|
|
|
});
|
2026-03-25 14:37:20 +08:00
|
|
|
const newObject = await cloneObjects([object]).then(v => v[0])
|
|
|
|
|
canvas.add(newObject)
|
|
|
|
|
canvas.renderAll()
|
|
|
|
|
return canvas
|
|
|
|
|
}
|
|
|
|
|
// 处理绘制图像
|
|
|
|
|
async handleDrawImage(object: fabric.Object) {
|
|
|
|
|
const tcanvas = await this.createStaticCanvas(this.demoObject)
|
|
|
|
|
tcanvas.add(object)
|
2026-03-25 11:27:24 +08:00
|
|
|
tcanvas.renderAll();
|
|
|
|
|
const canvas = getObjectAlphaToCanvas(tcanvas, null, 0, this.rgba);
|
2026-03-25 14:37:20 +08:00
|
|
|
this.replaceDemoObject(canvas)
|
|
|
|
|
}
|
|
|
|
|
// 处理图像删除
|
|
|
|
|
async handleRemoveImage(object: fabric.Object) {
|
|
|
|
|
const tcanvas = await this.createStaticCanvas(this.demoObject)
|
|
|
|
|
const rcanvas = await this.createStaticCanvas(object)
|
|
|
|
|
const tempCanvas = rcanvas.toCanvasElement();
|
|
|
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
|
const imageData2 = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
|
|
|
|
const canvas = getObjectAlphaToCanvas(tcanvas, imageData2, 0, this.rgba);
|
|
|
|
|
this.replaceDemoObject(canvas)
|
|
|
|
|
}
|
|
|
|
|
// 替换框选对象
|
|
|
|
|
async replaceDemoObject(canvas) {
|
|
|
|
|
const image = new fabric.Image(canvas, this.demoObjectDefault);
|
2026-03-25 11:27:24 +08:00
|
|
|
this.canvasManager.canvas.add(image)
|
|
|
|
|
this.canvasManager.canvas.remove(this.demoObject);
|
|
|
|
|
this.demoObject = image;
|
|
|
|
|
this.canvasManager.canvas.renderAll()
|
|
|
|
|
}
|
2026-03-25 14:37:20 +08:00
|
|
|
|
2026-03-25 11:27:24 +08:00
|
|
|
mouseDownEvent(e) {
|
|
|
|
|
const tool = this.toolManager.currentTool.value
|
|
|
|
|
const tools = [OperationType.AISELECT_ADD, OperationType.AISELECT_REMOVE]
|
|
|
|
|
if (!tools.includes(tool)) return;
|
|
|
|
|
|
|
|
|
|
this.isDragging = true
|
|
|
|
|
this.startX = e.absolutePointer.x
|
|
|
|
|
this.startY = e.absolutePointer.y
|
|
|
|
|
this.createIndicatorObject()
|
|
|
|
|
}
|
2026-03-13 11:18:36 +08:00
|
|
|
mouseMoveEvent(e) {
|
|
|
|
|
if (!this.isDragging) return;
|
|
|
|
|
var width = e.absolutePointer.x - this.startX
|
|
|
|
|
var height = e.absolutePointer.y - this.startY
|
|
|
|
|
var left = this.startX
|
|
|
|
|
var top = this.startY
|
2026-03-20 13:23:00 +08:00
|
|
|
if (width < 0) {
|
2026-03-13 11:18:36 +08:00
|
|
|
left += width
|
|
|
|
|
width = -width
|
|
|
|
|
}
|
2026-03-20 13:23:00 +08:00
|
|
|
if (height < 0) {
|
2026-03-13 11:18:36 +08:00
|
|
|
top += height
|
|
|
|
|
height = -height
|
|
|
|
|
}
|
2026-03-25 11:27:24 +08:00
|
|
|
this.indicatorObject.set({ width, height, left, top })
|
2026-03-13 11:18:36 +08:00
|
|
|
this.canvasManager.canvas.renderAll()
|
|
|
|
|
}
|
|
|
|
|
mouseUpEvent(e) {
|
|
|
|
|
if (!this.isDragging) return;
|
|
|
|
|
this.isDragging = false;
|
2026-03-25 11:27:24 +08:00
|
|
|
const object = this.indicatorObject.toJSON("evented")
|
2026-03-13 11:18:36 +08:00
|
|
|
if (object.width === 0) object.width = 100
|
|
|
|
|
if (object.height === 0) object.height = 100
|
2026-03-25 11:27:24 +08:00
|
|
|
this.clearIndicatorObject()
|
2026-03-13 11:18:36 +08:00
|
|
|
this.canvasManager.canvas.renderAll()
|
|
|
|
|
|
2026-03-25 14:37:20 +08:00
|
|
|
const rect = new fabric.Rect({
|
|
|
|
|
left: object.left,
|
|
|
|
|
top: object.top,
|
|
|
|
|
width: object.width,
|
|
|
|
|
height: object.height,
|
|
|
|
|
fill: '#f00',
|
|
|
|
|
strokeWidth: 0,
|
|
|
|
|
})
|
|
|
|
|
const currentTool = this.toolManager.currentTool.value
|
|
|
|
|
if (currentTool === OperationType.AISELECT_ADD) {
|
|
|
|
|
this.handleDrawImage(rect)
|
|
|
|
|
} else if (currentTool === OperationType.AISELECT_REMOVE) {
|
|
|
|
|
this.handleRemoveImage(rect)
|
|
|
|
|
}
|
2026-03-20 13:23:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadImageToObject(url) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
fabric.Image.fromURL(url, (img) => {
|
|
|
|
|
resolve(img);
|
|
|
|
|
}, { crossOrigin: "anonymous" });// 防止污染
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-25 11:27:24 +08:00
|
|
|
rgba = { r: 255, g: 0, b: 0, a: 127.5 };
|
2026-03-20 13:23:00 +08:00
|
|
|
selectionStyle = {
|
|
|
|
|
stroke: "rgba(255, 77, 71, 1)",
|
|
|
|
|
strokeWidth: 1.5,
|
|
|
|
|
strokeDashArray: [4, 4],
|
2026-03-23 16:43:08 +08:00
|
|
|
fill: "transparent",
|
2026-03-20 13:23:00 +08:00
|
|
|
strokeUniform: true, // 保持描边宽度不随缩放改变
|
2026-03-23 16:43:08 +08:00
|
|
|
selectable: false,
|
|
|
|
|
evented: false,
|
|
|
|
|
absolutePositioned: true,
|
2026-03-20 13:23:00 +08:00
|
|
|
};
|
|
|
|
|
async createSelectbox() {
|
2026-03-25 14:37:20 +08:00
|
|
|
if (!this.demoObject) return
|
2026-03-25 11:27:24 +08:00
|
|
|
const fobject = this.demoObject
|
|
|
|
|
this.clearDemoObject()
|
|
|
|
|
const canvas = getObjectAlphaToCanvas(fobject, null, 0, { r: 255, g: 0, b: 0, a: 255 });
|
2026-03-20 13:23:00 +08:00
|
|
|
const scaleY = fobject.scaleY
|
|
|
|
|
const scaleX = fobject.scaleX
|
|
|
|
|
const top = fobject.top
|
|
|
|
|
const left = fobject.left
|
|
|
|
|
const arr = traceImageContour(canvas);
|
|
|
|
|
let minX = fobject.width;
|
|
|
|
|
let minY = fobject.height;
|
|
|
|
|
const str = arr.map((v) => {
|
|
|
|
|
if (v.x < minX) minX = v.x;
|
|
|
|
|
if (v.y < minY) minY = v.y;
|
|
|
|
|
return `${v.x} ${v.y}`
|
|
|
|
|
}).join(" L ");
|
|
|
|
|
const path = new fabric.Path(`M ${str} z`);
|
|
|
|
|
path.set({
|
2026-03-23 16:43:08 +08:00
|
|
|
left: left + minX,
|
|
|
|
|
top: top + minY,
|
2026-03-20 13:23:00 +08:00
|
|
|
scaleX: scaleX,
|
|
|
|
|
scaleY: scaleY,
|
|
|
|
|
...this.selectionStyle,
|
|
|
|
|
});
|
2026-03-23 16:43:08 +08:00
|
|
|
const group = await this.layerManager.createGroupLayer({
|
|
|
|
|
clipPath: path,
|
2026-03-25 14:37:20 +08:00
|
|
|
top: path.top + path.height / 2,
|
|
|
|
|
left: path.left + path.width / 2,
|
2026-03-23 16:43:08 +08:00
|
|
|
}, false, false)
|
|
|
|
|
const rect = await this.layerManager.createRectLayer({
|
|
|
|
|
width: path.width,
|
|
|
|
|
height: path.height,
|
|
|
|
|
left: left + minX,
|
|
|
|
|
top: top + minY,
|
|
|
|
|
fill: "rgba(255, 186, 186, 0.5)",
|
|
|
|
|
info: { parentId: group.info.id },
|
|
|
|
|
}, false, true)
|
|
|
|
|
await this.canvasManager.updateSubLayerClipPath()
|
|
|
|
|
await this.layerManager.updateLayerThumbnailsById(rect.info.id, "", false)
|
|
|
|
|
await this.layerManager.updateLayerThumbnailsById(group.info.id, rect.thumbnail)
|
|
|
|
|
this.stateManager.recordState()
|
|
|
|
|
this.toolManager.setTool(OperationType.SELECT)
|
2026-03-13 11:18:36 +08:00
|
|
|
}
|
2026-03-20 13:23:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-25 11:27:24 +08:00
|
|
|
dispose() {
|
|
|
|
|
this.clear()
|
|
|
|
|
}
|
2026-03-13 11:18:36 +08:00
|
|
|
}
|