// fabric-annotation.service.ts
import { Injectable } from '@angular/core';
import { fabric } from 'fabric';
import { v4 as uuidv4 } from 'uuid';
// GeoJSON相关接口
export interface GeoJSONFeature {
type: 'Feature';
properties: {
[key: string]: any;
};
geometry: {
type: 'Polygon' | 'Point' | 'LineString' | 'MultiPolygon';
coordinates: any[] | any[][] | any[][][];
};
}
export interface GeoJSONFeatureCollection {
type: 'FeatureCollection';
name?: string;
features: GeoJSONFeature[];
}
export interface AnnotationShape {
id: number;
type: string;
originalCoords: any;
vertices: Array<{x: number, y: number, inBounds: boolean}>;
}
export interface ImagePosition {
x: number;
y: number;
scale: number;
width: number;
height: number;
}
export interface CanvasCoords {
x: number;
y: number;
inBounds: boolean;
imgWidth: number;
imgHeight: number;
}
// export interface SavedShape {
// type: string;
// left: number;
// top: number;
// width?: number;
// height?: number;
// radius?: number;
// points?: Array<{x: number, y: number}>;
// fill: string;
// stroke: string;
// strokeWidth: number;
// angle: number;
// scaleX: number;
// scaleY: number;
// originX: string;
// originY: string;
// flipX: boolean;
// flipY: boolean;
// opacity: number;
// strokeDashArray?: number[];
// // 自定义属性
// customType?: string;
// absolutePoints?: Array<{x: number, y: number}>;
// }
@Injectable({
providedIn: 'root',
})
export class FabricAnnotationService {
private canvas: any = null;
private currentTool: string = 'move';
private isDrawing: boolean = false;
private startPoint: any = null;
private tempShape: any = null;
private isDragging: boolean = false;
private lastPosX: number = 0;
private lastPosY: number = 0;
// 斜矩形绘制相关
private isDrawingSkewedRect: boolean = false;
private skewedRectStartPoint: any = null;
private tempSkewedLine: any = null;
private tempSkewedRect: any = null;
// 多边形绘制相关
private isDrawingPolygon: boolean = false;
private currentPolygonPoints: Array<{ x: number; y: number }> = [];
private tempPolygon: any = null;
private isNearFirstPoint: boolean = false;
// 当前画笔颜色
private currentColor: string = '#ff0000';
// 存储背景图片对象
private backgroundImage: any = null;
//当前元素
currentElement:any = null;
constructor() {}
/**
* 初始化画布
*/
initCanvas(canvasId: string): any {
this.canvas = new fabric.Canvas(canvasId);
this.setupEventListeners();
return this.canvas;
}
/**
* 设置画布背景图片
*/
setBackgroundImage(imageUrl: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.canvas) {
reject(new Error('Canvas not initialized'));
return;
}
fabric.Image.fromURL(
imageUrl,
(img: any) => {
img.set({
left: 0,
top: 0,
selectable: false,
evented: false,
});
this.backgroundImage = img;
this.canvas?.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas));
resolve();
},
{ crossOrigin: 'anonymous' }
);
});
}
/**
* 设置当前工具
*/
setCurrentTool(tool: 'move' | 'rect' | 'square' | 'skewedRect' | 'circle' | 'polygon'): void {
this.currentTool = tool;
if (!this.canvas) return;
switch (tool) {
case 'move':
this.canvas.selection = true;
this.canvas.defaultCursor = 'grab';
break;
case 'rect':
case 'square':
case 'skewedRect':
case 'circle':
case 'polygon':
this.canvas.selection = false;
this.canvas.defaultCursor = 'crosshair';
break;
}
}
/**
* 设置画笔颜色
*/
setBrushColor(color: string): void {
this.currentColor = color;
}
/**
* 获取当前工具
*/
getCurrentTool(): string {
return this.currentTool;
}
/**
* 清空画布
*/
clearCanvas(): void {
if (!this.canvas) return;
// 移除所有对象(包括临时形状)
this.canvas.getObjects().forEach((obj: any) => {
if (obj !== this.backgroundImage) {
this.canvas?.remove(obj);
}
});
// 重置视图
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.canvas.setZoom(1);
// 重置所有绘制状态
this.resetAllDrawingStates();
}
/**
* 重置视图
*/
resetAllDrawing(){
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.canvas.setZoom(1);
}
/**
* 获取图片在当前视图中的实际位置
*/
getImageActualPosition(): ImagePosition | null {
if (!this.backgroundImage || !this.canvas) return null;
const vpt: any = this.canvas.viewportTransform;
if (!vpt) return { x: 0, y: 0, scale: 1, width: 0, height: 0 };
const actualX = this.backgroundImage.left * vpt[0] + vpt[4];
const actualY = this.backgroundImage.top * vpt[3] + vpt[5];
return {
x: actualX,
y: actualY,
scale: vpt[0],
width: this.backgroundImage.width * vpt[0],
height: this.backgroundImage.height * vpt[3],
};
}
/**
* 将画布坐标转换为相对于图片原始左上角的坐标
*/
canvasToImageCoords(canvasX: number, canvasY: number): CanvasCoords | null {
if (!this.backgroundImage || !this.canvas) return null;
const vpt: any = this.canvas.viewportTransform;
if (!vpt) return null;
const originalX = (canvasX - vpt[4]) / vpt[0];
const originalY = (canvasY - vpt[5]) / vpt[3];
const relativeX = originalX;
const relativeY = originalY;
const imgWidth = this.backgroundImage.width || 800;
const imgHeight = this.backgroundImage.height || 600;
const inBounds = relativeX >= 0 && relativeX <= imgWidth && relativeY >= 0 && relativeY <= imgHeight;
return {
x: relativeX,
y: relativeY,
inBounds: inBounds,
imgWidth: imgWidth,
imgHeight: imgHeight,
};
}
/**
* 计算形状相对于图片原始左上角的坐标
*/
getShapeImageCoords(shape: any): any {
if (shape.type === 'polygon' || shape.type === 'polyline' || shape.type === 'skewed-rect') {
const points = shape.points || [];
const vertices = points.map((point: any) => {
const coords = this.canvasToImageCoords(shape.left + point.x, shape.top + point.y);
return coords;
});
return {
vertices: vertices,
type: shape.type,
angle: shape.angle || 0,
};
} else if (shape.type === 'circle') {
const center = this.canvasToImageCoords(shape.left, shape.top);
if (!center) return null;
const radius = shape.radius;
return {
center: center,
radius: radius,
type: 'circle',
angle: shape.angle || 0,
};
} else {
const topLeft = this.canvasToImageCoords(shape.left, shape.top);
const topRight = this.canvasToImageCoords(shape.left + (shape.width || 0), shape.top);
const bottomLeft = this.canvasToImageCoords(shape.left, shape.top + (shape.height || 0));
const bottomRight = this.canvasToImageCoords(shape.left + (shape.width || 0), shape.top + (shape.height || 0));
const centerX = this.canvasToImageCoords(
shape.left + (shape.width || 0) / 2,
shape.top + (shape.height || 0) / 2
);
if (!topLeft || !topRight || !bottomLeft || !bottomRight || !centerX) {
return null;
}
return {
topLeft: topLeft,
topRight: topRight,
bottomLeft: bottomLeft,
bottomRight: bottomRight,
width: shape.width || 0,
height: shape.height || 0,
centerX: centerX,
type: shape.type,
angle: shape.angle || 0,
};
}
}
/**
* 获取所有图形的顶点坐标(相对于图片原始左上角)
*/
getAllShapeVertices(): AnnotationShape[] {
if (!this.canvas) return [];
const objects = this.canvas.getObjects();
const shapes = objects.filter(
(obj: any) =>
obj.type === 'rect' ||
obj.type === 'circle' ||
obj.type === 'polygon' ||
obj.type === 'polyline' ||
obj.type === 'skewed-rect'
);
const result: AnnotationShape[] = [];
shapes.forEach((shape: any, index: number) => {
const coords = this.getShapeImageCoords(shape);
if (!coords) return;
const shapeData: AnnotationShape = {
id: index + 1,
type: shape.type,
originalCoords: coords,
vertices: [],
};
if (shape.type === 'polygon' || shape.type === 'polyline' || shape.type === 'skewed-rect') {
shapeData.vertices = coords.vertices;
} else if (shape.type === 'circle') {
const center = coords.center;
if (!center) return;
const radius = coords.radius;
const angleRad = (shape.angle * Math.PI) / 180;
shapeData.vertices = [];
for (let i = 0; i < 8; i++) {
const angle = (i * 45 * Math.PI) / 180 + angleRad;
const x = center.x + radius * Math.cos(angle);
const y = center.y + radius * Math.sin(angle);
shapeData.vertices.push({ x: x, y: y, inBounds: center.inBounds });
}
} else {
const centerX = coords.centerX;
if (!centerX) return;
const width = coords.width;
const height = coords.height;
const angleRad = ((shape.angle || 0) * Math.PI) / 180;
const halfWidth = width / 2;
const halfHeight = height / 2;
const corners = [
{ x: -halfWidth, y: -halfHeight },
{ x: halfWidth, y: -halfHeight },
{ x: halfWidth, y: halfHeight },
{ x: -halfWidth, y: halfHeight },
];
shapeData.vertices = corners.map((corner) => {
const rotatedX = corner.x * Math.cos(angleRad) - corner.y * Math.sin(angleRad);
const rotatedY = corner.x * Math.sin(angleRad) + corner.y * Math.cos(angleRad);
const absX = centerX.x + rotatedX;
const absY = centerX.y + rotatedY;
const inBounds = absX >= 0 && absX <= coords.imgWidth && absY >= 0 && absY <= coords.imgHeight;
return {
x: absX,
y: absY,
inBounds: inBounds,
};
});
}
result.push(shapeData);
});
return result;
}
/**
* 从统一格式创建图形
*/
private createShapeFromUnified(feature: any): void {
if (!this.canvas) return;
let obj: any;
const shape = feature.properties;
if (shape.type === 'circle') {
// 创建圆形
obj = new fabric.Circle({
left: shape.left,
top: shape.top,
radius: shape.radius || 50,
fill: shape.fill,
stroke: shape.stroke,
strokeWidth: shape.strokeWidth,
angle: shape.angle || 0,
scaleX: shape.scaleX || 1,
scaleY: shape.scaleY || 1,
originX: shape.originX || 'left',
originY: shape.originY || 'top',
flipX: shape.flipX || false,
flipY: shape.flipY || false,
opacity: shape.opacity || 1,
strokeDashArray: shape.strokeDashArray,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
type: 'circle',
});
} else {
// 创建多边形(包括原来的矩形)
obj = new fabric.Polygon(shape.points || [], {
left: shape.left,
top: shape.top,
fill: shape.fill,
stroke: shape.stroke,
strokeWidth: shape.strokeWidth,
angle: shape.angle || 0,
scaleX: shape.scaleX || 1,
scaleY: shape.scaleY || 1,
originX: shape.originX || 'left',
originY: shape.originY || 'top',
flipX: shape.flipX || false,
flipY: shape.flipY || false,
opacity: shape.opacity || 1,
strokeDashArray: shape.strokeDashArray,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
type: shape.customType || 'polygon', // 恢复原始类型
});
}
if (obj) {
obj.id = feature.id;
this.canvas.add(obj);
}
}
/**
* 将所有图形转换为统一格式(多边形格式)
*/
shapesToUnified(colorObj) {
if (!this.canvas) return [];
const objects = this.canvas.getObjects();
const shapes: any[] = [];
const features: any = [];
objects.forEach((obj: any) => {
if (obj !== this.backgroundImage) {
// 不保存背景图片
const shapeData: any = {
type: 'polygon', // 统一使用polygon类型
left: parseInt(obj.left),
top: parseInt(obj.top),
fill: obj.fill,
stroke: obj.stroke,
strokeWidth: obj.strokeWidth,
angle: obj.angle || 0,
scaleX: obj.scaleX || 1,
scaleY: obj.scaleY || 1,
originX: obj.originX || 'left',
originY: obj.originY || 'top',
flipX: obj.flipX || false,
flipY: obj.flipY || false,
opacity: obj.opacity || 1,
strokeDashArray: obj.strokeDashArray || undefined,
...colorObj
};
const feature = {id: obj.id,type: "Feature", properties:{}, "geometry": { type: "Polygon", coordinates:<any>[]}};
// 根据原始对象类型处理
if (obj.type === 'rect' || obj.type === 'square') {
// 将矩形转换为多边形顶点
const width = obj.width * obj.scaleX;
const height = obj.height * obj.scaleY;
const angleRad = ((obj.angle || 0) * Math.PI) / 180;
// 计算四个顶点相对于中心点的坐标
// const halfWidth = width / 2;
// const halfHeight = height / 2;
const corners = [
{ x: 0, y: 0 }, // 左上
{ x: width, y: 0 }, // 右上
{ x: width, y: height }, // 右下
{ x: 0, y: height }, // 左下
];
// 应用旋转
const rotatedCorners = corners.map((corner) => {
const rotatedX = corner.x * Math.cos(angleRad) - corner.y * Math.sin(angleRad);
const rotatedY = corner.x * Math.sin(angleRad) + corner.y * Math.cos(angleRad);
return {
x: rotatedX,
y: rotatedY,
};
});
const absolutePoints = rotatedCorners.map(corner => ({
x: obj.left + corner.x,
y: obj.top + corner.y
}));
shapeData.points = rotatedCorners;
shapeData.customType = obj.type; // 保留原始类型信息
// shapeData.absolutePoints = absolutePoints;
feature.properties = shapeData;
feature.geometry.coordinates = [absolutePoints.map(v=>[parseInt(v.x),parseInt(v.y)])];
} else if (obj.type === 'circle') {
shapeData.type = 'circle';
shapeData.radius = obj.radius;
shapeData.customType = 'circle';
feature.properties = shapeData;
// feature.geometry.coordinates = absolutePoints;
} else if (obj.type === 'polygon' || obj.type === 'skewed-rect') {
shapeData.points = obj.points ? obj.points.map((p: any) => ({ x: parseInt(p.x), y: parseInt(p.y) })) : [];
// shapeData.absolutePoints = shapeData.points;
shapeData.customType = obj.type;
feature.properties = shapeData;
if(shapeData.points) {
feature.geometry.coordinates = [shapeData.points.map(v=>[parseInt(v.x),parseInt(v.y)])];
}
}
shapes.push(shapeData);
features.push(feature);
}
});
return {
type: "FeatureCollection",
features:features
}
// return shapes;
}
/**
* 从统一格式恢复图形
*/
loadFromUnified(shapes: any): void {
if (!this.canvas) return;
// 清空当前画布(不包括背景图片)
this.canvas.getObjects().forEach((obj: any) => {
if (obj !== this.backgroundImage) {
this.canvas?.remove(obj);
}
});
// 重新创建图形
shapes.forEach((shape) => {
this.createShapeFromUnified(shape);
});
this.canvas.renderAll();
}
/**
* 保存所有图形到本地存储(统一格式)
*/
saveShapesToLocalStorage(key: string = 'fabric_unified_shapes'): void {
const shapes = this.shapesToUnified({});
localStorage.setItem(key, JSON.stringify(shapes));
}
/**
* 从本地存储加载图形(统一格式)
*/
loadShapesFromLocalStorage(key: string = 'fabric_unified_shapes'): void {
const shapesStr = localStorage.getItem(key);
if (!shapesStr) return;
try {
// const shapes: SavedShape[] = JSON.parse(shapesStr);
const geojson = JSON.parse(shapesStr);
const features:any[] = geojson.features;
this.loadFromUnified(features);
} catch (error) {
console.error('Failed to load shapes from localStorage:', error);
}
}
/**
* 保存所有图形到本地存储
*/
// saveShapesToLocalStorage(key: string = 'fabric_shapes'): void {
// if (!this.canvas) return;
// const objects = this.canvas.getObjects();
// const shapes: SavedShape[] = [];
// objects.forEach((obj: any) => {
// if (obj !== this.backgroundImage) { // 不保存背景图片
// const shapeData: SavedShape = {
// type: obj.type,
// left: obj.left,
// top: obj.top,
// fill: obj.fill,
// stroke: obj.stroke,
// strokeWidth: obj.strokeWidth,
// angle: obj.angle || 0,
// scaleX: obj.scaleX || 1,
// scaleY: obj.scaleY || 1,
// originX: obj.originX || 'left',
// originY: obj.originY || 'top',
// flipX: obj.flipX || false,
// flipY: obj.flipY || false,
// opacity: obj.opacity || 1,
// strokeDashArray: obj.strokeDashArray || undefined
// };
// // 根据对象类型添加特定属性
// if (obj.type === 'rect' || obj.type === 'circle') {
// shapeData.width = obj.width;
// shapeData.height = obj.height;
// }
// if (obj.type === 'circle') {
// shapeData.radius = obj.radius;
// }
// if (obj.type === 'polygon' || obj.type === 'polyline' || obj.type === 'skewed-rect') {
// // 对于斜矩形,我们保存原始点坐标而不是变换后的坐标
// if (obj.type === 'skewed-rect') {
// // 保存原始点(相对于左上角的点)
// const originalPoints = obj.points.map((p: any) => ({
// x: p.x,
// y: p.y
// }));
// shapeData.points = originalPoints;
// shapeData.customType = 'skewed-rect';
// } else {
// shapeData.points = obj.points ? [...obj.points] : [];
// }
// }
// shapes.push(shapeData);
// }
// });
// localStorage.setItem(key, JSON.stringify(shapes));
// }
/**
* 从本地存储恢复图形
*/
// loadShapesFromLocalStorage(key: string = 'fabric_shapes'): void {
// if (!this.canvas) return;
// const savedShapes = localStorage.getItem(key);
// if (!savedShapes) return;
// try {
// const shapes: SavedShape[] = JSON.parse(savedShapes);
// // 先清空当前画布(不包括背景图片)
// this.canvas.getObjects().forEach((obj: any) => {
// if (obj !== this.backgroundImage) {
// this.canvas?.remove(obj);
// }
// });
// // 重新创建图形
// shapes.forEach((shape: SavedShape) => {
// this.createShapeFromData(shape);
// });
// this.canvas.renderAll();
// } catch (error) {
// console.error('Failed to load shapes from localStorage:', error);
// }
// }
/**
* 从数据创建图形
*/
private createShapeFromData(shape: any): void {
if (!this.canvas) return;
let obj: any;
switch (shape.type) {
case 'rect':
obj = new fabric.Rect({
left: shape.left,
top: shape.top,
width: shape.width,
height: shape.height,
fill: shape.fill,
stroke: shape.stroke,
strokeWidth: shape.strokeWidth,
angle: shape.angle,
scaleX: shape.scaleX,
scaleY: shape.scaleY,
originX: shape.originX,
originY: shape.originY,
flipX: shape.flipX,
flipY: shape.flipY,
opacity: shape.opacity,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
});
break;
case 'circle':
obj = new fabric.Circle({
left: shape.left,
top: shape.top,
radius: shape.radius,
fill: shape.fill,
stroke: shape.stroke,
strokeWidth: shape.strokeWidth,
angle: shape.angle,
scaleX: shape.scaleX,
scaleY: shape.scaleY,
originX: shape.originX,
originY: shape.originY,
flipX: shape.flipX,
flipY: shape.flipY,
opacity: shape.opacity,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
});
break;
case 'polygon':
case 'polyline':
obj = new fabric.Polygon(shape.points || [], {
left: shape.left,
top: shape.top,
fill: shape.fill,
stroke: shape.stroke,
strokeWidth: shape.strokeWidth,
angle: shape.angle,
scaleX: shape.scaleX,
scaleY: shape.scaleY,
originX: shape.originX,
originY: shape.originY,
flipX: shape.flipX,
flipY: shape.flipY,
opacity: shape.opacity,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
});
break;
case 'skewed-rect':
// 为斜矩形创建多边形,但保留自定义类型
obj = new fabric.Polygon(shape.points || [], {
left: shape.left,
top: shape.top,
fill: shape.fill,
stroke: shape.stroke,
strokeWidth: shape.strokeWidth,
angle: shape.angle,
scaleX: shape.scaleX,
scaleY: shape.scaleY,
originX: shape.originX,
originY: shape.originY,
flipX: shape.flipX,
flipY: shape.flipY,
opacity: shape.opacity,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
});
// 设置自定义类型以便识别
(obj as any).type = 'skewed-rect';
break;
default:
console.warn(`Unknown shape type: ${shape.type}`);
return;
}
this.canvas.add(obj);
}
/**
* 完成多边形绘制
*/
finishPolygon(): void {
if (!this.isDrawingPolygon || this.currentPolygonPoints.length < 3) {
if (this.currentPolygonPoints.length < 3) {
alert('多边形至少需要3个顶点!');
}
return;
}
this.isDrawingPolygon = false;
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
this.tempPolygon = null;
}
const finalPolygon = new fabric.Polygon(this.currentPolygonPoints, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.1)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
});
(finalPolygon as any).id = uuidv4();//多边形增加id
this.canvas?.add(finalPolygon);
this.currentPolygonPoints = [];
this.isNearFirstPoint = false;
}
/**
* 取消多边形绘制
*/
cancelPolygon(): void {
this.isDrawingPolygon = false;
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
this.tempPolygon = null;
}
this.currentPolygonPoints = [];
this.isNearFirstPoint = false;
}
/**
* 重置多边形状态
*/
private resetPolygonState(): void {
this.isDrawingPolygon = false;
this.currentPolygonPoints = [];
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
this.tempPolygon = null;
}
this.isNearFirstPoint = false;
}
/**
* 重置斜矩形状态
*/
private resetSkewedRectState(): void {
this.isDrawingSkewedRect = false;
this.skewedRectStartPoint = null;
if (this.tempSkewedLine) {
this.canvas?.remove(this.tempSkewedLine);
this.tempSkewedLine = null;
}
if (this.tempSkewedRect) {
this.canvas?.remove(this.tempSkewedRect);
this.tempSkewedRect = null;
}
}
/**
* 重置所有绘制状态
*/
private resetAllDrawingStates(): void {
this.isDrawing = false;
if (this.tempShape) {
this.canvas?.remove(this.tempShape);
this.tempShape = null;
}
this.resetPolygonState();
this.resetSkewedRectState();
}
/**
* 设置事件监听器
*/
private setupEventListeners(): void {
if (!this.canvas) return;
// 鼠标滚轮缩放
this.canvas.on('mouse:wheel', (opt: any) => {
if (this.currentTool !== 'move') {
opt.e.preventDefault();
return;
}
const delta = opt.e.deltaY;
let zoom = this.canvas?.getZoom() || 1;
zoom = zoom + delta * -0.001;
zoom = Math.min(Math.max(0.1, zoom), 5);
const point = new fabric.Point(opt.e.offsetX, opt.e.offsetY);
this.canvas?.zoomToPoint(point, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
});
// 鼠标按下事件
this.canvas.on('mouse:down', (opt: any) => {
if (this.currentTool === 'move') {
this.isDragging = true;
this.lastPosX = opt.e.clientX;
this.lastPosY = opt.e.clientY;
} else if (this.currentTool === 'skewedRect') {
const target = this.canvas?.findTarget(opt.e);
if (target && ['rect', 'circle', 'polygon', 'polyline', 'skewed-rect'].includes(target.type)) {
this.canvas?.setActiveObject(target);
return;
}
this.isDrawingSkewedRect = true;
this.skewedRectStartPoint = this.canvas?.getPointer(opt.e) || null;
if (this.skewedRectStartPoint) {
this.tempSkewedLine = new fabric.Line(
[
this.skewedRectStartPoint.x,
this.skewedRectStartPoint.y,
this.skewedRectStartPoint.x,
this.skewedRectStartPoint.y,
],
{
stroke: this.currentColor,
strokeWidth: 1,
strokeDashArray: [5, 5],
selectable: false,
evented: false,
}
);
this.canvas?.add(this.tempSkewedLine);
}
} else if (this.currentTool === 'polygon') {
const target = this.canvas?.findTarget(opt.e);
if (target && ['rect', 'circle', 'polygon', 'polyline', 'skewed-rect'].includes(target.type)) {
this.canvas?.setActiveObject(target);
return;
}
if (this.isNearFirstPoint && this.currentPolygonPoints.length >= 3) {
this.finishPolygon();
return;
}
if (!this.isDrawingPolygon) {
this.isDrawingPolygon = true;
this.currentPolygonPoints = [];
}
const pointer = this.canvas?.getPointer(opt.e);
if (pointer) {
this.currentPolygonPoints.push({
x: pointer.x,
y: pointer.y,
});
}
if (this.currentPolygonPoints.length >= 2) {
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
}
this.tempPolygon = new fabric.Polyline(this.currentPolygonPoints, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.1)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
evented: false,
});
this.canvas?.add(this.tempPolygon);
this.canvas?.renderAll();
}
} else if (['rect', 'square', 'circle'].includes(this.currentTool)) {
const target = this.canvas?.findTarget(opt.e);
if (target && ['rect', 'circle', 'polygon', 'polyline', 'skewed-rect'].includes(target.type)) {
this.canvas?.setActiveObject(target);
return;
}
this.isDrawing = true;
this.startPoint = this.canvas?.getPointer(opt.e) || null;
if (this.currentTool === 'circle') {
const radius = 1;
this.tempShape = new fabric.Circle({
left: this.startPoint?.x || 0,
top: this.startPoint?.y || 0,
radius: radius,
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.2)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
});
} else {
this.tempShape = new fabric.Rect({
left: this.startPoint?.x || 0,
top: this.startPoint?.y || 0,
width: 1,
height: 1,
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.2)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
});
}
(this.tempShape as any).id = uuidv4();//斜矩形增加id
this.canvas?.add(this.tempShape);
}
});
// 鼠标移动事件
this.canvas.on('mouse:move', (opt: any) => {
// 检查是否有选中的对象
const activeObject = this.canvas?.getActiveObject();
if (this.isDragging && this.currentTool === 'move') {
if (!activeObject || activeObject === this.backgroundImage) {
const e = opt.e;
const vpt: any = this.canvas?.viewportTransform;
if (vpt) {
vpt[4] += e.clientX - this.lastPosX;
vpt[5] += e.clientY - this.lastPosY;
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
this.canvas?.requestRenderAll();
}
}
} else if (this.isDrawingSkewedRect && this.currentTool === 'skewedRect' && this.skewedRectStartPoint) {
const pointer = this.canvas?.getPointer(opt.e);
if (pointer && this.tempSkewedLine) {
this.tempSkewedLine.set({
x2: pointer.x,
y2: pointer.y,
});
this.tempSkewedLine.setCoords();
this.canvas?.renderAll();
}
if (pointer && this.skewedRectStartPoint) {
const dx = pointer.x - this.skewedRectStartPoint.x;
const dy = pointer.y - this.skewedRectStartPoint.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length > 0) {
const perpX = -dy / length;
const perpY = dx / length;
if (this.tempSkewedRect) {
this.canvas?.remove(this.tempSkewedRect);
this.tempSkewedRect = null;
}
const height = length / 3;
const width = length;
const points = [
{ x: this.skewedRectStartPoint.x, y: this.skewedRectStartPoint.y },
{ x: pointer.x, y: pointer.y },
{ x: pointer.x + perpX * height, y: pointer.y + perpY * height },
{ x: this.skewedRectStartPoint.x + perpX * height, y: this.skewedRectStartPoint.y + perpY * height },
];
this.tempSkewedRect = new fabric.Polygon(points, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.2)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
evented: false,
type: 'skewed-rect', // 确保类型为斜矩形
});
this.canvas?.add(this.tempSkewedRect);
this.canvas?.renderAll();
}
}
} else if (this.isDrawingPolygon && this.currentTool === 'polygon') {
const pointer = this.canvas?.getPointer(opt.e);
if (!pointer) return;
const updatedPoints = [...this.currentPolygonPoints];
this.isNearFirstPoint = false;
if (this.currentPolygonPoints.length >= 3) {
const firstPoint = this.currentPolygonPoints[0];
const distance = Math.sqrt(Math.pow(pointer.x - firstPoint.x, 2) + Math.pow(pointer.y - firstPoint.y, 2));
if (distance < 5) {
this.isNearFirstPoint = true;
this.canvas.defaultCursor = 'pointer';
} else {
this.isNearFirstPoint = false;
this.canvas.defaultCursor = 'crosshair';
}
} else {
this.canvas.defaultCursor = 'crosshair';
}
if (this.currentPolygonPoints.length > 0) {
updatedPoints.push({
x: pointer.x,
y: pointer.y,
});
}
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
}
if (this.isNearFirstPoint && this.currentPolygonPoints.length >= 3) {
this.tempPolygon = new fabric.Polyline(updatedPoints, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.3)`,
stroke: this.currentColor === '#ff0000' ? '#ff6666' : this.currentColor,
strokeWidth: 3,
selectable: false,
evented: false,
});
} else {
this.tempPolygon = new fabric.Polyline(updatedPoints, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.1)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
evented: false,
});
}
this.canvas?.add(this.tempPolygon);
this.canvas?.renderAll();
} else if (this.isDrawing && this.tempShape) {
const pointer = this.canvas?.getPointer(opt.e);
if (!pointer || !this.startPoint) return;
if (this.currentTool === 'circle') {
const radius = Math.sqrt(
Math.pow(pointer.x - this.startPoint.x, 2) + Math.pow(pointer.y - this.startPoint.y, 2)
);
(this.tempShape as any).set({
radius: radius,
});
} else {
let width = pointer.x - this.startPoint.x;
let height = pointer.y - this.startPoint.y;
if (this.currentTool === 'square') {
const absWidth = Math.abs(width);
const absHeight = Math.abs(height);
const size = Math.min(absWidth, absHeight);
width = width >= 0 ? size : -size;
height = height >= 0 ? size : -size;
}
const left = width < 0 ? pointer.x : this.startPoint.x;
const top = height < 0 ? pointer.y : this.startPoint.y;
this.tempShape.set({
left: left,
top: top,
width: Math.abs(width),
height: Math.abs(height),
});
}
this.tempShape.setCoords();
this.canvas?.renderAll();
}
});
// 鼠标抬起事件
this.canvas.on('mouse:up', () => {
if (this.isDragging && this.currentTool === 'move') {
this.isDragging = false;
} else if (this.isDrawing && this.tempShape) {
this.isDrawing = false;
let isTooSmall = false;
if (this.currentTool === 'circle') {
isTooSmall = (this.tempShape as any).radius < 5;
} else {
isTooSmall = Math.abs(this.tempShape.width || 0) < 5 || Math.abs(this.tempShape.height || 0) < 5;
}
if (isTooSmall) {
this.canvas?.remove(this.tempShape);
} else {
this.tempShape.set({
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.1)`,
stroke: this.currentColor,
});
this.canvas?.setActiveObject(this.tempShape);
}
this.tempShape = null;
} else if (this.isDrawingSkewedRect && this.currentTool === 'skewedRect') {
this.isDrawingSkewedRect = false;
if (this.tempSkewedLine) {
this.canvas?.remove(this.tempSkewedLine);
this.tempSkewedLine = null;
}
if (this.tempSkewedRect) {
// 保存原始点坐标而不是变换后的坐标
const originalPoints = this.tempSkewedRect.points.map((p: any) => ({
x: p.x,
y: p.y,
}));
const finalSkewedRect = new fabric.Polygon(originalPoints, {
left: this.tempSkewedRect.left,
top: this.tempSkewedRect.top,
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.2)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
type: 'skewed-rect',
});
(finalSkewedRect as any).id = uuidv4();//斜矩形增加id
this.canvas?.add(finalSkewedRect);
this.canvas?.setActiveObject(finalSkewedRect);
this.canvas?.remove(this.tempSkewedRect);
this.tempSkewedRect = null;
}
} else if (
this.isDrawingPolygon &&
this.currentTool === 'polygon' &&
this.isNearFirstPoint &&
this.currentPolygonPoints.length >= 3
) {
this.finishPolygon();
}
});
// 鼠标右键事件
this.canvas.on('mouse:down', (opt: any) => {
if (opt.e.button === 2 && this.currentTool === 'polygon' && this.isDrawingPolygon) {
if (this.currentPolygonPoints.length > 0) {
this.currentPolygonPoints.pop();
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
}
if (this.currentPolygonPoints.length >= 2) {
this.tempPolygon = new fabric.Polyline(this.currentPolygonPoints, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.1)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
evented: false,
});
this.canvas?.add(this.tempPolygon);
} else if (this.currentPolygonPoints.length === 1) {
this.tempPolygon = new fabric.Polyline(this.currentPolygonPoints, {
fill: `rgba(${parseInt(this.currentColor.slice(1, 3), 16)}, ${parseInt(
this.currentColor.slice(3, 5),
16
)}, ${parseInt(this.currentColor.slice(5, 7), 16)}, 0.1)`,
stroke: this.currentColor,
strokeWidth: 2,
selectable: false,
evented: false,
});
this.canvas?.add(this.tempPolygon);
} else {
this.tempPolygon = null;
}
this.canvas?.renderAll();
}
opt.e.preventDefault();
return false;
}
});
// 鼠标双击重置视图
this.canvas.on('mouse:dblclick', () => {
if (this.currentTool !== 'move') return;
this.canvas?.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.canvas?.setZoom(1);
});
}
/**
* 从GeoJSON数据导入多边形
*/
loadFromGeoJSON(geojson: any): void {
if (!this.canvas) return;
// 清空当前画布(不包括背景图片)
this.canvas.getObjects().forEach((obj: any) => {
if (obj !== this.backgroundImage) {
this.canvas?.remove(obj);
}
});
// 遍历GeoJSON中的所有要素
geojson.features.forEach((feature) => {
if (feature.geometry.type === 'Polygon') {
this.createPolygonFromGeoJSON(feature);
}
});
this.canvas.renderAll();
}
/**
* 从GeoJSON要素创建多边形
*/
private createPolygonFromGeoJSON(feature: GeoJSONFeature): void {
if (!this.canvas || feature.geometry.type !== 'Polygon') return;
// 获取坐标数组
const coordinates = feature.geometry.coordinates[0]; // 取外环
// 移除闭合点(最后一个点通常与第一个点相同)
const coordsWithoutClosing = coordinates.slice(0, -1);
// 转换坐标:GeoJSON坐标通常是 [x, y, z] 格式
const points = coordsWithoutClosing.map((coord) => ({
x: coord[0], // x坐标
y: -coord[1], // y坐标需要翻转(GeoJSON的y轴方向与Canvas相反)
}));
// 计算边界框以确定中心点
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
points.forEach((point) => {
if (point.x < minX) minX = point.x;
if (point.x > maxX) maxX = point.x;
if (point.y < minY) minY = point.y;
if (point.y > maxY) maxY = point.y;
});
const centerX = minX;
const centerY = minY;
let color = '#FF0000'; // 默认红色
// 创建多边形
const polygon = new fabric.Polygon(points, {
left: centerX,
top: centerY,
fill: `rgba(${parseInt(color.slice(1, 3), 16)}, ${parseInt(color.slice(3, 5), 16)}, ${parseInt(
color.slice(5, 7),
16
)}, 0.3)`,
stroke: color,
strokeWidth: 2,
selectable: true,
hasControls: true,
hasRotatingPoint: true,
lockUniScaling: false,
type: 'polygon'
});
this.canvas.add(polygon);
}
getAllItems(){
return this.canvas.getObjects();
}
getLastItem(){
const objects = this.canvas.getObjects();
if(Array.isArray(objects) && objects.length > 0) {
return objects[objects.length - 1];
}
}
//=====================================有关交互部分===================================================//
searchActiveObject(){
const activeObject = this.canvas?.getActiveObject();
if (!activeObject || activeObject === this.backgroundImage) {
return null;
}else{
return activeObject;
}
}
/**
* 根据ID查找元素
*/
findElementById(id: string): fabric.Object | undefined {
if (!this.canvas) return undefined;
const objects = this.canvas.getObjects();
return objects.find(obj => (obj as any).id === id);
}
/**
* 点击列表项时定位到元素
*/
locateElementById(id: string): void {
const element = this.findElementById(id);
// debugger;
if (!element || !this.canvas) return;
// 高亮显示元素
// this.highlightElement(element);
// 选中元素
this.selectElement(element);
}
/**
* 选中元素
*/
selectElement(element: fabric.Object): void {
if (!this.canvas) return;
// 清除当前选中状态
this.canvas.discardActiveObject();
// 设置元素为可选中状态(如果尚未设置)
element.set({
selectable: true,
evented: true
});
// 选中元素
this.canvas.setActiveObject(element);
// 重新渲染以显示选中状态
this.canvas.renderAll();
}
/**
* 高亮显示元素
*/
highlightElement(element: fabric.Object): void {
// 暂时改变元素样式
const originalFill = element.fill;
const originalStroke = element.stroke;
// const originalStrokeWidth = (element as any).strokeWidth;
element.set({
fill: 'rgba(255, 255, 0, 0.5)', // 黄色半透明填充
stroke: '#FFFF00', // 黄色边框
// strokeWidth: (originalStrokeWidth || 2) + 2 // 增加边框宽度
});
this.canvas?.renderAll();
// 2秒后恢复原状
setTimeout(() => {
element.set({
fill: originalFill,
stroke: originalStroke,
// strokeWidth: originalStrokeWidth
});
this.canvas?.renderAll();
}, 1000);
}
/**
* 删除元素
*/
deleteElementById(id: string): boolean {
const element = this.findElementById(id);
if (!element || !this.canvas) return false;
this.canvas.remove(element);
// this.elementInfoMap.delete(id);
return true;
}
/**
* 切换元素可见性
*/
toggleElementVisibility(id: string): boolean {
const element = this.findElementById(id);
if (!element) return false;
const newVisibility = !element.visible;
element.set({ visible: newVisibility });
// 更新元素信息
this.canvas?.renderAll();
return newVisibility;
}
// /**
// * 更新元素信息
// */
// updateElementInfo(id: string, updates: Partial<ElementInfo>): void {
// const info = this.elementInfoMap.get(id);
// if (info) {
// Object.assign(info, updates);
// this.elementInfoMap.set(id, info);
// }
// }
// /**
// * 获取元素信息
// */
// getElementInfo(id: string): ElementInfo | undefined {
// return this.elementInfoMap.get(id);
// }
//=====================================有关交互部分===================================================//
}
普通图片标注工具
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
相关阅读更多精彩内容
- 不会UI的开发不是好开发,技不压身,能自己解决的不麻烦UI小姐姐,将本人用的比较多的UI工具推荐给大家 一) 图片...
- 以上实现了工具类,当然需要一个入口函数,将工具类保存为SimpleBBoxLabeling.py,新建Run_De...
- 图片标注(Image Annotation)是物体检测等工作的基础,就是使用矩形框标注出图片中的物体,同时指定合适...