// 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);
// 重置所有繪制狀態(tài)
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],
};
}
/**
* 將畫布坐標轉(zhuǎn)換為相對于圖片原始左上角的坐標
*/
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;
}
/**
* 從統(tǒng)一格式創(chuàng)建圖形
*/
private createShapeFromUnified(feature: any): void {
if (!this.canvas) return;
let obj: any;
const shape = feature.properties;
if (shape.type === 'circle') {
// 創(chuàng)建圓形
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 {
// 創(chuàng)建多邊形(包括原來的矩形)
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);
}
}
/**
* 將所有圖形轉(zhuǎn)換為統(tǒng)一格式(多邊形格式)
*/
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', // 統(tǒng)一使用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>[]}};
// 根據(jù)原始對象類型處理
if (obj.type === 'rect' || obj.type === 'square') {
// 將矩形轉(zhuǎn)換為多邊形頂點
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 }, // 左下
];
// 應用旋轉(zhuǎn)
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;
}
/**
* 從統(tǒng)一格式恢復圖形
*/
loadFromUnified(shapes: any): void {
if (!this.canvas) return;
// 清空當前畫布(不包括背景圖片)
this.canvas.getObjects().forEach((obj: any) => {
if (obj !== this.backgroundImage) {
this.canvas?.remove(obj);
}
});
// 重新創(chuàng)建圖形
shapes.forEach((shape) => {
this.createShapeFromUnified(shape);
});
this.canvas.renderAll();
}
/**
* 保存所有圖形到本地存儲(統(tǒng)一格式)
*/
saveShapesToLocalStorage(key: string = 'fabric_unified_shapes'): void {
const shapes = this.shapesToUnified({});
localStorage.setItem(key, JSON.stringify(shapes));
}
/**
* 從本地存儲加載圖形(統(tǒng)一格式)
*/
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
// };
// // 根據(jù)對象類型添加特定屬性
// 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);
// }
// });
// // 重新創(chuàng)建圖形
// shapes.forEach((shape: SavedShape) => {
// this.createShapeFromData(shape);
// });
// this.canvas.renderAll();
// } catch (error) {
// console.error('Failed to load shapes from localStorage:', error);
// }
// }
/**
* 從數(shù)據(jù)創(chuàng)建圖形
*/
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':
// 為斜矩形創(chuàng)建多邊形,但保留自定義類型
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;
}
/**
* 重置多邊形狀態(tài)
*/
private resetPolygonState(): void {
this.isDrawingPolygon = false;
this.currentPolygonPoints = [];
if (this.tempPolygon) {
this.canvas?.remove(this.tempPolygon);
this.tempPolygon = null;
}
this.isNearFirstPoint = false;
}
/**
* 重置斜矩形狀態(tài)
*/
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;
}
}
/**
* 重置所有繪制狀態(tài)
*/
private resetAllDrawingStates(): void {
this.isDrawing = false;
if (this.tempShape) {
this.canvas?.remove(this.tempShape);
this.tempShape = null;
}
this.resetPolygonState();
this.resetSkewedRectState();
}
/**
* 設置事件監(jiān)聽器
*/
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數(shù)據(jù)導入多邊形
*/
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要素創(chuàng)建多邊形
*/
private createPolygonFromGeoJSON(feature: GeoJSONFeature): void {
if (!this.canvas || feature.geometry.type !== 'Polygon') return;
// 獲取坐標數(shù)組
const coordinates = feature.geometry.coordinates[0]; // 取外環(huán)
// 移除閉合點(最后一個點通常與第一個點相同)
const coordsWithoutClosing = coordinates.slice(0, -1);
// 轉(zhuǎn)換坐標:GeoJSON坐標通常是 [x, y, z] 格式
const points = coordsWithoutClosing.map((coord) => ({
x: coord[0], // x坐標
y: -coord[1], // y坐標需要翻轉(zhuǎn)(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'; // 默認紅色
// 創(chuàng)建多邊形
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;
}
}
/**
* 根據(jù)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;
// 清除當前選中狀態(tài)
this.canvas.discardActiveObject();
// 設置元素為可選中狀態(tài)(如果尚未設置)
element.set({
selectable: true,
evented: true
});
// 選中元素
this.canvas.setActiveObject(element);
// 重新渲染以顯示選中狀態(tài)
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 // 增加邊框?qū)挾? });
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);
// }
//=====================================有關交互部分===================================================//
}
普通圖片標注工具
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
相關閱讀更多精彩內(nèi)容
- 不會UI的開發(fā)不是好開發(fā),技不壓身,能自己解決的不麻煩UI小姐姐,將本人用的比較多的UI工具推薦給大家 一) 圖片...
- 以上實現(xiàn)了工具類,當然需要一個入口函數(shù),將工具類保存為SimpleBBoxLabeling.py,新建Run_De...
- 圖片標注(Image Annotation)是物體檢測等工作的基礎,就是使用矩形框標注出圖片中的物體,同時指定合適...