image-annotation-drawer 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3333 +1 @@
1
- (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ImageAnnotationDrawer = global.ImageAnnotationDrawer || {}));
5
- })(this, (function (exports) { 'use strict';
6
-
7
- /**
8
- * 工具函数模块
9
- * 包含几何计算、坐标转换等通用工具函数
10
- */
11
- /**
12
- * 检测点是否在矩形内
13
- * 支持负宽高的情况(从右下往左上画)
14
- */
15
- function isPointInRect(point, rect) {
16
- // 处理负宽高的情况
17
- const x = rect.width >= 0 ? rect.start.x : rect.start.x + rect.width;
18
- const y = rect.height >= 0 ? rect.start.y : rect.start.y + rect.height;
19
- const w = Math.abs(rect.width);
20
- const h = Math.abs(rect.height);
21
- return point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h;
22
- }
23
- /**
24
- * 检测点是否在多边形内(射线法)
25
- */
26
- function isPointInPolygon(point, polygon) {
27
- if (polygon.length < 3)
28
- return false;
29
- let inside = false;
30
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
31
- const xi = polygon[i].point.x;
32
- const yi = polygon[i].point.y;
33
- const xj = polygon[j].point.x;
34
- const yj = polygon[j].point.y;
35
- const intersect = yi > point.y !== yj > point.y &&
36
- point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
37
- if (intersect)
38
- inside = !inside;
39
- }
40
- return inside;
41
- }
42
- /**
43
- * 从 URL 解析图片类型
44
- * 支持 png, jpg, jpeg, webp, gif, bmp, svg 等常见格式
45
- */
46
- function getImageTypeFromUrl(urlString) {
47
- try {
48
- const url = new URL(urlString);
49
- const pathname = url.pathname.toLowerCase();
50
- // 支持的图片格式
51
- if (pathname.endsWith(".png"))
52
- return "png";
53
- if (pathname.endsWith(".jpg"))
54
- return "jpg";
55
- if (pathname.endsWith(".jpeg"))
56
- return "jpeg";
57
- if (pathname.endsWith(".webp"))
58
- return "webp";
59
- if (pathname.endsWith(".gif"))
60
- return "gif";
61
- if (pathname.endsWith(".bmp"))
62
- return "bmp";
63
- if (pathname.endsWith(".svg"))
64
- return "svg";
65
- // 默认返回 jpeg
66
- return "jpeg";
67
- }
68
- catch (_a) {
69
- return "jpeg";
70
- }
71
- }
72
- /**
73
- * 将 base64 字符串转换为 File 对象
74
- */
75
- function base64ToFile(base64Data, filename) {
76
- const parts = base64Data.split(";base64,");
77
- const contentType = parts[0].split(":")[1];
78
- const raw = window.atob(parts[1]);
79
- const rawLength = raw.length;
80
- const uInt8Array = new Uint8Array(rawLength);
81
- for (let i = 0; i < rawLength; ++i) {
82
- uInt8Array[i] = raw.charCodeAt(i);
83
- }
84
- const blob = new Blob([uInt8Array], { type: contentType });
85
- return new File([blob], filename, { type: contentType });
86
- }
87
- /**
88
- * 将 base64 字符串转换为 Blob 对象(用于上传)
89
- */
90
- function base64ToBlob(base64) {
91
- return new Promise((resolve, reject) => {
92
- const parts = base64.split(";base64,");
93
- if (parts.length < 2) {
94
- reject(new Error("Invalid base64 string"));
95
- return;
96
- }
97
- const mime = parts[0].split(":")[1];
98
- const byteString = atob(parts[1]);
99
- const arrayBuffer = new ArrayBuffer(byteString.length);
100
- const uint8Array = new Uint8Array(arrayBuffer);
101
- for (let i = 0; i < byteString.length; i++) {
102
- uint8Array[i] = byteString.charCodeAt(i);
103
- }
104
- resolve(new Blob([arrayBuffer], { type: mime }));
105
- });
106
- }
107
- /**
108
- * 计算缩放增量
109
- */
110
- function getZoomDelta(currentScale, zoomIn) {
111
- // 使用动态缩放增量
112
- if (currentScale < 2) {
113
- return zoomIn ? 0.1 : -0.1;
114
- }
115
- else if (currentScale < 5) {
116
- return zoomIn ? 0.05 : -0.05;
117
- }
118
- else {
119
- return zoomIn ? 0.02 : -0.02;
120
- }
121
- }
122
-
123
- /**
124
- * 视图管理模块
125
- * 负责视口尺寸、缩放、偏移、边界约束等视图相关功能
126
- */
127
- class ViewportManager {
128
- constructor() {
129
- // 视口尺寸
130
- this.width = 0;
131
- this.height = 0;
132
- // 原始图片尺寸
133
- this.originalWidth = 0;
134
- this.originalHeight = 0;
135
- // 视图变换属性
136
- this.scale = 1;
137
- this.minScale = 1;
138
- this.maxScale = 10;
139
- this.offset = { x: 0, y: 0 };
140
- // 初始视图状态
141
- this.initialScale = 1;
142
- this.initialOffset = { x: 0, y: 0 };
143
- this.isInitialScale = true;
144
- // 边界约束
145
- this.minOffsetX = 0;
146
- this.minOffsetY = 0;
147
- this.maxOffsetX = 0;
148
- this.maxOffsetY = 0;
149
- }
150
- /**
151
- * 设置视口尺寸
152
- */
153
- setSize(width, height) {
154
- this.width = width;
155
- this.height = height;
156
- }
157
- /**
158
- * 设置原始图片尺寸
159
- */
160
- setOriginalSize(width, height) {
161
- this.originalWidth = width;
162
- this.originalHeight = height;
163
- }
164
- /**
165
- * 计算初始视图参数
166
- */
167
- calculateInitialView() {
168
- if (this.originalWidth === 0 || this.originalHeight === 0)
169
- return;
170
- // 计算缩放比例以适应容器
171
- const scaleX = this.width / this.originalWidth;
172
- const scaleY = this.height / this.originalHeight;
173
- this.scale = Math.min(scaleX, scaleY);
174
- this.initialScale = this.scale;
175
- // 设置最小缩放为初始缩放比例
176
- this.minScale = this.initialScale;
177
- // 计算居中位置
178
- this.offset = {
179
- x: (this.width - this.originalWidth * this.scale) / 2,
180
- y: (this.height - this.originalHeight * this.scale) / 2,
181
- };
182
- this.initialOffset = Object.assign({}, this.offset);
183
- // 计算边界约束
184
- this.calculateBoundaries();
185
- // 标记为初始状态
186
- this.isInitialScale = true;
187
- }
188
- /**
189
- * 计算边界约束
190
- */
191
- calculateBoundaries() {
192
- const imgScaledWidth = this.originalWidth * this.scale;
193
- const imgScaledHeight = this.originalHeight * this.scale;
194
- // 最大偏移量(图像完全在视口内)
195
- this.maxOffsetX = 0;
196
- this.maxOffsetY = 0;
197
- // 最小偏移量(图像完全覆盖视口)
198
- this.minOffsetX = this.width - imgScaledWidth;
199
- this.minOffsetY = this.height - imgScaledHeight;
200
- // 确保最小值不大于最大值
201
- if (this.minOffsetX > this.maxOffsetX) {
202
- [this.minOffsetX, this.maxOffsetX] = [this.maxOffsetX, this.minOffsetX];
203
- }
204
- if (this.minOffsetY > this.maxOffsetY) {
205
- [this.minOffsetY, this.maxOffsetY] = [this.maxOffsetY, this.minOffsetY];
206
- }
207
- }
208
- /**
209
- * 确保视图在合理范围内
210
- */
211
- constrainViewport() {
212
- // 约束X轴
213
- if (this.offset.x > this.maxOffsetX) {
214
- this.offset.x = this.maxOffsetX;
215
- }
216
- else if (this.offset.x < this.minOffsetX) {
217
- this.offset.x = this.minOffsetX;
218
- }
219
- // 约束Y轴
220
- if (this.offset.y > this.maxOffsetY) {
221
- this.offset.y = this.maxOffsetY;
222
- }
223
- else if (this.offset.y < this.minOffsetY) {
224
- this.offset.y = this.minOffsetY;
225
- }
226
- }
227
- /**
228
- * 将画布坐标转换为图像坐标
229
- */
230
- toImageCoordinates(canvasX, canvasY) {
231
- return {
232
- x: (canvasX - this.offset.x) / this.scale,
233
- y: (canvasY - this.offset.y) / this.scale,
234
- };
235
- }
236
- /**
237
- * 将图像坐标转换为画布坐标
238
- */
239
- toCanvasCoordinates(imgX, imgY) {
240
- return {
241
- x: this.offset.x + imgX * this.scale,
242
- y: this.offset.y + imgY * this.scale,
243
- };
244
- }
245
- /**
246
- * 更新缩放比例
247
- */
248
- updateScale(newScale, centerX, centerY) {
249
- const oldScale = this.scale;
250
- // 计算缩放前中心点在图像中的位置
251
- const centerImgX = (centerX - this.offset.x) / oldScale;
252
- const centerImgY = (centerY - this.offset.y) / oldScale;
253
- // 更新缩放比例
254
- this.scale = Math.max(this.minScale, Math.min(newScale, this.maxScale));
255
- // 计算缩放后中心点应该的位置
256
- const newCenterX = centerImgX * this.scale + this.offset.x;
257
- const newCenterY = centerImgY * this.scale + this.offset.y;
258
- // 调整偏移量以保持中心点不变
259
- this.offset.x += centerX - newCenterX;
260
- this.offset.y += centerY - newCenterY;
261
- // 更新边界约束
262
- this.calculateBoundaries();
263
- this.constrainViewport();
264
- // 更新初始状态标志
265
- this.isInitialScale = this.scale === this.initialScale;
266
- }
267
- /**
268
- * 重置视图到初始状态
269
- */
270
- resetToInitial() {
271
- this.scale = this.initialScale;
272
- this.offset = Object.assign({}, this.initialOffset);
273
- this.calculateBoundaries();
274
- this.isInitialScale = true;
275
- }
276
- /**
277
- * 更新偏移量(用于拖拽)
278
- */
279
- updateOffset(dx, dy) {
280
- this.offset.x += dx;
281
- this.offset.y += dy;
282
- this.constrainViewport();
283
- }
284
- /**
285
- * 重置所有状态
286
- */
287
- reset() {
288
- this.scale = 1;
289
- this.minScale = 1;
290
- this.offset = { x: 0, y: 0 };
291
- this.initialScale = 1;
292
- this.initialOffset = { x: 0, y: 0 };
293
- this.isInitialScale = true;
294
- this.minOffsetX = 0;
295
- this.minOffsetY = 0;
296
- this.maxOffsetX = 0;
297
- this.maxOffsetY = 0;
298
- }
299
- }
300
-
301
- /**
302
- * 标注管理模块
303
- * 负责矩形、多边形标注的存储、绘制、选中、移动、调整大小等功能
304
- */
305
- class AnnotationManager {
306
- constructor(viewport) {
307
- this.viewport = viewport;
308
- // 标注记录列表
309
- this.recordList = [];
310
- // 当前操作
311
- this.operate = {
312
- type: "rect",
313
- data: [],
314
- status: "pending",
315
- };
316
- // 绘制状态
317
- this.isDrawing = false;
318
- this.drawStartPoint = { x: 0, y: 0 };
319
- this.tempPolygonPoint = null;
320
- // 选中状态
321
- this.selectedAnnotation = null;
322
- this.isMovingAnnotation = false;
323
- this.isResizing = false;
324
- this.activeHandle = null;
325
- this.annotationMoveStart = { x: 0, y: 0 };
326
- // 调整大小用的原始数据
327
- this.originalRect = null;
328
- this.originalPolygon = null;
329
- // 删除历史记录(用于撤销删除)
330
- this.deleteHistory = [];
331
- // 样式配置
332
- this.strokeStyle = "red";
333
- this.lineWidth = 5;
334
- this.lineStyle = 'solid';
335
- this.vertexStyle = {
336
- size: 8,
337
- fillColor: 'red',
338
- strokeColor: 'white',
339
- strokeWidth: 2,
340
- shape: 'circle'
341
- };
342
- this.selectionStyle = {
343
- strokeColor: "#00D9FF",
344
- fillColor: "rgba(0,217,255,0.15)",
345
- handleSize: 12,
346
- handleColor: "#00D9FF",
347
- };
348
- // 颜色配置
349
- this.colorConfig = {
350
- rect: "red",
351
- polygon: "red",
352
- default: "red"
353
- };
354
- this.applyColorConfig();
355
- }
356
- /**
357
- * 设置颜色配置
358
- */
359
- setColorConfig(config) {
360
- if (typeof config === 'string') {
361
- this.colorConfig = {
362
- rect: config,
363
- polygon: config,
364
- default: config
365
- };
366
- }
367
- else {
368
- this.colorConfig = Object.assign(Object.assign({}, this.colorConfig), config);
369
- }
370
- this.applyColorConfig();
371
- }
372
- /**
373
- * 获取颜色配置
374
- */
375
- getColorConfig() {
376
- return Object.assign({}, this.colorConfig);
377
- }
378
- /**
379
- * 应用颜色配置到当前样式
380
- */
381
- applyColorConfig() {
382
- this.strokeStyle = this.colorConfig.default || "red";
383
- }
384
- /**
385
- * 获取指定类型的颜色
386
- */
387
- getColorForType(type) {
388
- return this.colorConfig[type] || this.colorConfig.default || "red";
389
- }
390
- /**
391
- * 获取当前样式(用于创建新标注时保存)
392
- */
393
- getCurrentStyle() {
394
- return {
395
- strokeColor: this.strokeStyle,
396
- lineWidth: this.lineWidth,
397
- lineStyle: this.lineStyle,
398
- vertexStyle: Object.assign({}, this.vertexStyle)
399
- };
400
- }
401
- /**
402
- * 设置边线样式
403
- */
404
- setLineStyle(style) {
405
- this.lineStyle = style;
406
- }
407
- /**
408
- * 设置顶点样式
409
- */
410
- setVertexStyle(style) {
411
- this.vertexStyle = Object.assign(Object.assign({}, this.vertexStyle), style);
412
- // 同步更新颜色
413
- if (style.fillColor) {
414
- this.strokeStyle = style.fillColor;
415
- }
416
- }
417
- /**
418
- * 获取边线样式
419
- */
420
- getLineStyle() {
421
- return this.lineStyle;
422
- }
423
- /**
424
- * 获取顶点样式
425
- */
426
- getVertexStyle() {
427
- return Object.assign({}, this.vertexStyle);
428
- }
429
- /**
430
- * 获取标注的样式(如果不存在则返回当前样式)
431
- */
432
- getAnnotationStyle(annotation) {
433
- return annotation.style || this.getCurrentStyle();
434
- }
435
- /**
436
- * 开始绘制矩形
437
- */
438
- startRectDrawing(startPoint) {
439
- this.isDrawing = true;
440
- this.drawStartPoint = startPoint;
441
- this.operate = {
442
- type: "rect",
443
- data: [{ start: Object.assign({}, startPoint), width: 0, height: 0 }],
444
- status: "pending",
445
- };
446
- }
447
- /**
448
- * 更新矩形绘制
449
- */
450
- updateRectDrawing(currentPoint) {
451
- if (!this.isDrawing || this.operate.type !== "rect")
452
- return;
453
- const rect = this.operate.data[0];
454
- rect.width = currentPoint.x - this.drawStartPoint.x;
455
- rect.height = currentPoint.y - this.drawStartPoint.y;
456
- }
457
- /**
458
- * 完成矩形绘制
459
- */
460
- finishRectDrawing() {
461
- if (!this.isDrawing || this.operate.type !== "rect")
462
- return false;
463
- const rect = this.operate.data[0];
464
- // 过滤无效矩形(太小)
465
- if (Math.abs(rect.width) > 5 && Math.abs(rect.height) > 5) {
466
- // 保存当前样式到标注
467
- const annotationWithStyle = Object.assign(Object.assign({}, this.operate), { style: this.getCurrentStyle() });
468
- this.recordList.push(annotationWithStyle);
469
- // 添加新标注后清空删除历史
470
- this.deleteHistory = [];
471
- }
472
- this.operate.data = [];
473
- this.isDrawing = false;
474
- return true;
475
- }
476
- /**
477
- * 开始绘制多边形
478
- */
479
- startPolygonDrawing(startPoint) {
480
- this.isDrawing = true;
481
- this.operate = {
482
- type: "polygon",
483
- data: [{ point: startPoint }],
484
- status: "pending",
485
- };
486
- this.tempPolygonPoint = startPoint;
487
- }
488
- /**
489
- * 添加多边形点
490
- */
491
- addPolygonPoint(point) {
492
- if (!this.isDrawing || this.operate.type !== "polygon")
493
- return false;
494
- // 避免添加重复点
495
- const lastPoint = this.operate.data.length > 0
496
- ? this.operate.data[this.operate.data.length - 1]
497
- : null;
498
- if (lastPoint &&
499
- Math.abs(lastPoint.point.x - point.x) <= 5 &&
500
- Math.abs(lastPoint.point.y - point.y) <= 5) {
501
- return false;
502
- }
503
- this.operate.data.push({ point });
504
- return true;
505
- }
506
- /**
507
- * 更新多边形临时点
508
- */
509
- updatePolygonTempPoint(point) {
510
- this.tempPolygonPoint = point;
511
- }
512
- /**
513
- * 完成多边形绘制
514
- */
515
- finishPolygonDrawing() {
516
- if (!this.isDrawing || this.operate.type !== "polygon")
517
- return false;
518
- // 过滤无效多边形(点数太少)
519
- if (this.operate.data.length >= 3) {
520
- this.operate.status = "fullfilled";
521
- // 保存当前样式到标注
522
- const annotationWithStyle = Object.assign(Object.assign({}, this.operate), { style: this.getCurrentStyle() });
523
- this.recordList.push(annotationWithStyle);
524
- // 添加新标注后清空删除历史
525
- this.deleteHistory = [];
526
- }
527
- this.operate = {
528
- type: "polygon",
529
- data: [],
530
- status: "pending",
531
- };
532
- this.isDrawing = false;
533
- this.tempPolygonPoint = null;
534
- return true;
535
- }
536
- /**
537
- * 取消当前绘制
538
- */
539
- cancelDrawing() {
540
- if (this.isDrawing && this.operate.type === "polygon" && this.operate.data.length >= 2) {
541
- // 如果多边形至少有两个点,保存当前进度
542
- this.operate.status = "fullfilled";
543
- // 保存当前样式到标注
544
- const annotationWithStyle = Object.assign(Object.assign({}, this.operate), { style: this.getCurrentStyle() });
545
- this.recordList.push(annotationWithStyle);
546
- }
547
- this.operate.data = [];
548
- this.isDrawing = false;
549
- this.tempPolygonPoint = null;
550
- }
551
- /**
552
- * 撤销操作
553
- */
554
- withdraw() {
555
- if (this.operate.data.length > 0) {
556
- // 撤销当前操作中的点
557
- if (this.operate.type === "polygon") {
558
- this.operate.data.pop();
559
- if (this.operate.data.length === 0) {
560
- this.isDrawing = false;
561
- }
562
- }
563
- else {
564
- this.operate.data = [];
565
- this.isDrawing = false;
566
- }
567
- return true;
568
- }
569
- else if (this.deleteHistory.length > 0) {
570
- // 优先恢复删除的标注
571
- const { annotation, index } = this.deleteHistory.pop();
572
- // 在原来的位置插入
573
- this.recordList.splice(index, 0, annotation);
574
- return true;
575
- }
576
- else if (this.recordList.length > 0) {
577
- // 撤销已完成的操作
578
- this.recordList.pop();
579
- return true;
580
- }
581
- return false;
582
- }
583
- /**
584
- * 选中指定索引的标注
585
- */
586
- selectAnnotation(index) {
587
- if (index < 0 || index >= this.recordList.length) {
588
- this.deselectAnnotation();
589
- return false;
590
- }
591
- const annotation = this.recordList[index];
592
- this.selectedAnnotation = {
593
- index,
594
- type: annotation.type,
595
- };
596
- return true;
597
- }
598
- /**
599
- * 取消选中
600
- */
601
- deselectAnnotation() {
602
- this.selectedAnnotation = null;
603
- this.isMovingAnnotation = false;
604
- this.isResizing = false;
605
- this.activeHandle = null;
606
- this.originalRect = null;
607
- this.originalPolygon = null;
608
- }
609
- /**
610
- * 删除选中的标注
611
- */
612
- deleteSelectedAnnotation() {
613
- if (!this.selectedAnnotation)
614
- return false;
615
- const index = this.selectedAnnotation.index;
616
- const annotation = this.recordList[index];
617
- // 保存删除的记录到历史(用于撤销)
618
- this.deleteHistory.push({
619
- annotation: Object.assign({}, annotation),
620
- index
621
- });
622
- // 删除标注
623
- this.recordList.splice(index, 1);
624
- this.deselectAnnotation();
625
- return true;
626
- }
627
- /**
628
- * 开始移动标注
629
- */
630
- startMovingAnnotation(e) {
631
- if (!this.selectedAnnotation)
632
- return false;
633
- this.isMovingAnnotation = true;
634
- this.annotationMoveStart = { x: e.clientX, y: e.clientY };
635
- return true;
636
- }
637
- /**
638
- * 移动选中的标注
639
- */
640
- moveSelectedAnnotation(dx, dy) {
641
- if (!this.selectedAnnotation)
642
- return false;
643
- const annotation = this.recordList[this.selectedAnnotation.index];
644
- if (!annotation)
645
- return false;
646
- if (annotation.type === "rect") {
647
- const rect = annotation.data[0];
648
- rect.start.x += dx;
649
- rect.start.y += dy;
650
- }
651
- else if (annotation.type === "polygon") {
652
- const polygon = annotation.data;
653
- polygon.forEach((p) => {
654
- p.point.x += dx;
655
- p.point.y += dy;
656
- });
657
- }
658
- return true;
659
- }
660
- /**
661
- * 完成标注移动
662
- */
663
- finishMovingAnnotation() {
664
- this.isMovingAnnotation = false;
665
- this.isResizing = false;
666
- this.activeHandle = null;
667
- this.originalRect = null;
668
- this.originalPolygon = null;
669
- }
670
- /**
671
- * 开始调整大小
672
- */
673
- startResizing(handle, _startPoint) {
674
- if (!this.selectedAnnotation)
675
- return false;
676
- this.activeHandle = handle;
677
- this.isResizing = true;
678
- const annotation = this.recordList[this.selectedAnnotation.index];
679
- if (annotation.type === "rect") {
680
- this.originalRect = Object.assign({}, annotation.data[0]);
681
- }
682
- else if (annotation.type === "polygon") {
683
- this.originalPolygon = annotation.data.map(p => ({ point: Object.assign({}, p.point) }));
684
- }
685
- return true;
686
- }
687
- /**
688
- * 调整矩形大小
689
- */
690
- resizeRect(currentPoint) {
691
- if (!this.selectedAnnotation || this.selectedAnnotation.type !== "rect" || !this.originalRect || !this.activeHandle) {
692
- return false;
693
- }
694
- const annotation = this.recordList[this.selectedAnnotation.index];
695
- const rect = annotation.data[0];
696
- const original = this.originalRect;
697
- // 原始矩形的四个角坐标
698
- const origLeft = original.start.x;
699
- const origRight = original.start.x + original.width;
700
- const origTop = original.start.y;
701
- const origBottom = original.start.y + original.height;
702
- // 根据当前控制点索引确定固定点
703
- let fixedX, fixedY;
704
- switch (this.activeHandle.index) {
705
- case 0:
706
- fixedX = origRight;
707
- fixedY = origBottom;
708
- break;
709
- case 1:
710
- fixedX = origLeft;
711
- fixedY = origBottom;
712
- break;
713
- case 2:
714
- fixedX = origRight;
715
- fixedY = origTop;
716
- break;
717
- case 3:
718
- fixedX = origLeft;
719
- fixedY = origTop;
720
- break;
721
- default: return false;
722
- }
723
- // 新矩形的边界由拖拽点和对角点决定
724
- const newLeft = Math.min(currentPoint.x, fixedX);
725
- const newRight = Math.max(currentPoint.x, fixedX);
726
- const newTop = Math.min(currentPoint.y, fixedY);
727
- const newBottom = Math.max(currentPoint.y, fixedY);
728
- rect.start.x = newLeft;
729
- rect.start.y = newTop;
730
- rect.width = newRight - newLeft;
731
- rect.height = newBottom - newTop;
732
- // 根据新的矩形边界,更新控制点索引(实现越过交换)
733
- const isLeft = currentPoint.x < fixedX;
734
- const isTop = currentPoint.y < fixedY;
735
- if (this.activeHandle.index === 0) {
736
- if (!isLeft && !isTop)
737
- this.activeHandle.index = 3;
738
- else if (!isLeft && isTop)
739
- this.activeHandle.index = 1;
740
- else if (isLeft && !isTop)
741
- this.activeHandle.index = 2;
742
- }
743
- else if (this.activeHandle.index === 1) {
744
- if (isLeft && !isTop)
745
- this.activeHandle.index = 2;
746
- else if (isLeft && isTop)
747
- this.activeHandle.index = 0;
748
- else if (!isLeft && !isTop)
749
- this.activeHandle.index = 3;
750
- }
751
- else if (this.activeHandle.index === 2) {
752
- if (!isLeft && isTop)
753
- this.activeHandle.index = 1;
754
- else if (isLeft && isTop)
755
- this.activeHandle.index = 0;
756
- else if (!isLeft && !isTop)
757
- this.activeHandle.index = 3;
758
- }
759
- else if (this.activeHandle.index === 3) {
760
- if (isLeft && isTop)
761
- this.activeHandle.index = 0;
762
- else if (!isLeft && isTop)
763
- this.activeHandle.index = 1;
764
- else if (isLeft && !isTop)
765
- this.activeHandle.index = 2;
766
- }
767
- // 更新 originalRect 为当前矩形,为下次拖拽做准备
768
- this.originalRect = Object.assign({}, rect);
769
- return true;
770
- }
771
- /**
772
- * 调整多边形顶点位置
773
- */
774
- resizePolygon(currentPoint) {
775
- if (!this.selectedAnnotation || this.selectedAnnotation.type !== "polygon" || !this.activeHandle) {
776
- return false;
777
- }
778
- const annotation = this.recordList[this.selectedAnnotation.index];
779
- const polygon = annotation.data;
780
- const vertexIndex = this.activeHandle.index;
781
- if (vertexIndex >= 0 && vertexIndex < polygon.length) {
782
- polygon[vertexIndex].point.x = currentPoint.x;
783
- polygon[vertexIndex].point.y = currentPoint.y;
784
- return true;
785
- }
786
- return false;
787
- }
788
- /**
789
- * 获取点击位置对应的标注
790
- */
791
- getAnnotationAtPoint(imgCoords) {
792
- // 从后往前遍历,优先选中上层标注
793
- for (let i = this.recordList.length - 1; i >= 0; i--) {
794
- const annotation = this.recordList[i];
795
- if (annotation.type === "rect") {
796
- const rect = annotation.data[0];
797
- if (isPointInRect(imgCoords, rect)) {
798
- return { index: i, type: "rect" };
799
- }
800
- }
801
- else if (annotation.type === "polygon") {
802
- const polygon = annotation.data;
803
- if (isPointInPolygon(imgCoords, polygon)) {
804
- return { index: i, type: "polygon" };
805
- }
806
- }
807
- }
808
- return null;
809
- }
810
- /**
811
- * 获取点击位置的控制点
812
- */
813
- getHandleAtPoint(offsetX, offsetY) {
814
- if (!this.selectedAnnotation)
815
- return null;
816
- const annotation = this.recordList[this.selectedAnnotation.index];
817
- const handleSize = this.selectionStyle.handleSize;
818
- const halfHandle = handleSize / 2;
819
- if (annotation.type === "rect") {
820
- const rect = annotation.data[0];
821
- const x = this.viewport.offset.x + rect.start.x * this.viewport.scale;
822
- const y = this.viewport.offset.y + rect.start.y * this.viewport.scale;
823
- const w = rect.width * this.viewport.scale;
824
- const h = rect.height * this.viewport.scale;
825
- // 四个角的控制点
826
- const corners = [
827
- { x: x, y: y, index: 0 },
828
- { x: x + w, y: y, index: 1 },
829
- { x: x, y: y + h, index: 2 },
830
- { x: x + w, y: y + h, index: 3 },
831
- ];
832
- for (const corner of corners) {
833
- if (offsetX >= corner.x - halfHandle &&
834
- offsetX <= corner.x + halfHandle &&
835
- offsetY >= corner.y - halfHandle &&
836
- offsetY <= corner.y + halfHandle) {
837
- return { type: "rect-corner", index: corner.index };
838
- }
839
- }
840
- }
841
- else if (annotation.type === "polygon") {
842
- const polygon = annotation.data;
843
- for (let i = 0; i < polygon.length; i++) {
844
- const p = polygon[i];
845
- const x = this.viewport.offset.x + p.point.x * this.viewport.scale;
846
- const y = this.viewport.offset.y + p.point.y * this.viewport.scale;
847
- if (offsetX >= x - halfHandle &&
848
- offsetX <= x + halfHandle &&
849
- offsetY >= y - halfHandle &&
850
- offsetY <= y + halfHandle) {
851
- return { type: "polygon-vertex", index: i };
852
- }
853
- }
854
- }
855
- return null;
856
- }
857
- /**
858
- * 清除所有标注
859
- */
860
- clear() {
861
- this.recordList = [];
862
- this.operate = {
863
- type: "rect",
864
- data: [],
865
- status: "pending",
866
- };
867
- this.isDrawing = false;
868
- this.tempPolygonPoint = null;
869
- this.deselectAnnotation();
870
- // 清空删除历史
871
- this.deleteHistory = [];
872
- }
873
- /**
874
- * 获取所有标注数据
875
- */
876
- getAnnotations() {
877
- return [...this.recordList];
878
- }
879
- /**
880
- * 获取当前选中的标注信息
881
- */
882
- getSelectedAnnotation() {
883
- if (!this.selectedAnnotation)
884
- return null;
885
- const annotation = this.recordList[this.selectedAnnotation.index];
886
- if (!annotation)
887
- return null;
888
- return {
889
- index: this.selectedAnnotation.index,
890
- type: this.selectedAnnotation.type,
891
- data: annotation,
892
- };
893
- }
894
- }
895
-
896
- /**
897
- * 文本标注模块
898
- * 负责文本标注的添加、编辑、移动、删除等功能
899
- */
900
- class TextAnnotationManager {
901
- constructor(viewport, container, ctx, renderCallback) {
902
- this.viewport = viewport;
903
- this.container = container;
904
- this.ctx = ctx;
905
- this.renderCallback = renderCallback;
906
- // 文本标注列表
907
- this.textAnnotations = [];
908
- // 编辑状态
909
- this.editingTextIndex = null;
910
- this.currentEditingIndex = null;
911
- this.isTextMoving = false;
912
- this.textMoveStart = { x: 0, y: 0 };
913
- this.textBeforeEditing = "";
914
- // 选中状态(非编辑状态下的选中)
915
- this.selectedTextIndex = null;
916
- // DOM 元素
917
- this.textInput = null;
918
- // 删除历史记录(用于撤销删除)
919
- this.deleteHistory = [];
920
- // 默认输入框样式
921
- this.defaultInputStyle = {
922
- border: "2px solid #00D9FF",
923
- borderRadius: "6px",
924
- padding: "6px 10px",
925
- fontSize: "16px",
926
- fontFamily: "Arial, sans-serif",
927
- backgroundColor: "#ffffff",
928
- color: "#333",
929
- boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
930
- minWidth: "60px",
931
- maxWidth: "200px"
932
- };
933
- // 样式配置
934
- this.textStyle = {
935
- font: "16px Arial",
936
- color: "#FFD700",
937
- padding: 6,
938
- backgroundColor: "rgba(0,0,0,0.6)",
939
- borderRadius: 4,
940
- selectedBorderColor: "#00D9FF",
941
- selectedBackgroundColor: "rgba(0,217,255,0.15)",
942
- inputStyle: Object.assign({}, this.defaultInputStyle)
943
- };
944
- this.createTextInput();
945
- }
946
- /**
947
- * 创建文本输入框
948
- */
949
- createTextInput() {
950
- this.textInput = document.createElement("input");
951
- this.textInput.type = "text";
952
- this.textInput.style.position = "absolute";
953
- this.textInput.style.zIndex = "99999";
954
- this.textInput.style.display = "none";
955
- this.textInput.style.outline = "none";
956
- this.textInput.style.width = "fit-content";
957
- this.textInput.placeholder = "输入文字...";
958
- // 应用自定义样式
959
- this.applyInputStyle();
960
- // 失去焦点时完成编辑
961
- this.textInput.addEventListener("blur", () => {
962
- this.finishEditing();
963
- // 触发重新渲染以显示文本标注
964
- if (this.renderCallback) {
965
- this.renderCallback();
966
- }
967
- });
968
- // 键盘事件处理
969
- this.textInput.addEventListener("keydown", (e) => {
970
- e.stopPropagation();
971
- if (e.key === "Enter") {
972
- this.finishEditing();
973
- // 触发重新渲染以显示文本标注
974
- if (this.renderCallback) {
975
- this.renderCallback();
976
- }
977
- }
978
- else if (e.key === "Escape") {
979
- e.preventDefault();
980
- this.cancelEditing();
981
- }
982
- else if (e.key === "Delete") {
983
- if (this.textInput.value === "") {
984
- e.preventDefault();
985
- this.deleteEditingAnnotation();
986
- }
987
- }
988
- });
989
- // 输入时自动调整宽度
990
- this.textInput.addEventListener("input", () => {
991
- this.adjustInputWidth();
992
- });
993
- this.container.appendChild(this.textInput);
994
- }
995
- /**
996
- * 应用输入框样式
997
- */
998
- applyInputStyle() {
999
- if (!this.textInput)
1000
- return;
1001
- const style = Object.assign(Object.assign({}, this.defaultInputStyle), this.textStyle.inputStyle);
1002
- this.textInput.style.border = style.border;
1003
- this.textInput.style.borderRadius = style.borderRadius;
1004
- this.textInput.style.padding = style.padding;
1005
- this.textInput.style.fontSize = style.fontSize;
1006
- this.textInput.style.fontFamily = style.fontFamily;
1007
- this.textInput.style.backgroundColor = style.backgroundColor;
1008
- this.textInput.style.color = style.color;
1009
- this.textInput.style.boxShadow = style.boxShadow;
1010
- this.textInput.style.minWidth = style.minWidth;
1011
- this.textInput.style.maxWidth = style.maxWidth;
1012
- }
1013
- /**
1014
- * 设置文本样式
1015
- */
1016
- setTextStyle(style) {
1017
- this.textStyle = Object.assign(Object.assign({}, this.textStyle), style);
1018
- this.applyInputStyle();
1019
- }
1020
- /**
1021
- * 设置输入框样式
1022
- */
1023
- setInputStyle(style) {
1024
- this.textStyle.inputStyle = Object.assign(Object.assign({}, this.textStyle.inputStyle), style);
1025
- this.applyInputStyle();
1026
- }
1027
- /**
1028
- * 设置选中态样式
1029
- */
1030
- setSelectionStyle(style) {
1031
- if (style.selectedBorderColor !== undefined) {
1032
- this.textStyle.selectedBorderColor = style.selectedBorderColor;
1033
- }
1034
- if (style.selectedBackgroundColor !== undefined) {
1035
- this.textStyle.selectedBackgroundColor = style.selectedBackgroundColor;
1036
- }
1037
- }
1038
- /**
1039
- * 获取当前文本样式(用于创建新标注时保存)
1040
- */
1041
- getCurrentStyle() {
1042
- return {
1043
- font: this.textStyle.font,
1044
- color: this.textStyle.color,
1045
- backgroundColor: this.textStyle.backgroundColor
1046
- };
1047
- }
1048
- /**
1049
- * 添加文本标注
1050
- */
1051
- addTextAnnotation(x, y, text = "") {
1052
- var _a;
1053
- // 测量文本尺寸
1054
- this.ctx.font = this.textStyle.font;
1055
- const measureText = text || "输入文字...";
1056
- const metrics = this.ctx.measureText(measureText);
1057
- // 计算实际文本高度(使用 actualBoundingBox 如果可用,否则使用字体大小)
1058
- let textHeight;
1059
- if (metrics.actualBoundingBoxAscent && metrics.actualBoundingBoxDescent) {
1060
- textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
1061
- }
1062
- else {
1063
- // 回退:使用字体大小
1064
- const fontSize = parseInt(((_a = this.textStyle.font.match(/\d+/)) === null || _a === void 0 ? void 0 : _a[0]) || "16");
1065
- textHeight = fontSize * 1.2;
1066
- }
1067
- const textAnnotation = {
1068
- position: { x, y },
1069
- text,
1070
- // 保存当前样式到标注
1071
- style: this.getCurrentStyle(),
1072
- width: Math.max(metrics.width, 60),
1073
- height: textHeight,
1074
- };
1075
- this.textAnnotations.push(textAnnotation);
1076
- const index = this.textAnnotations.length - 1;
1077
- // 添加新标注后清空删除历史
1078
- this.deleteHistory = [];
1079
- // 直接进入编辑模式
1080
- this.startEditing(index);
1081
- return index;
1082
- }
1083
- /**
1084
- * 更新文本标注内容
1085
- */
1086
- updateTextAnnotation(index, text) {
1087
- if (index < 0 || index >= this.textAnnotations.length)
1088
- return false;
1089
- const textAnnotation = this.textAnnotations[index];
1090
- textAnnotation.text = text;
1091
- // 更新文本尺寸
1092
- this.ctx.font = this.textStyle.font;
1093
- const metrics = this.ctx.measureText(text);
1094
- textAnnotation.width = metrics.width;
1095
- textAnnotation.height = parseInt(this.textStyle.font) * 1.2;
1096
- return true;
1097
- }
1098
- /**
1099
- * 移动文本标注位置
1100
- */
1101
- moveTextAnnotation(index, x, y) {
1102
- if (index < 0 || index >= this.textAnnotations.length)
1103
- return false;
1104
- this.textAnnotations[index].position = { x, y };
1105
- return true;
1106
- }
1107
- /**
1108
- * 删除文本标注
1109
- */
1110
- removeTextAnnotation(index) {
1111
- if (index < 0 || index >= this.textAnnotations.length)
1112
- return false;
1113
- const annotation = this.textAnnotations[index];
1114
- // 保存删除的记录到历史(用于撤销)
1115
- this.deleteHistory.push({
1116
- annotation: Object.assign({}, annotation),
1117
- index
1118
- });
1119
- this.textAnnotations.splice(index, 1);
1120
- // 如果正在编辑该标注,取消编辑
1121
- if (this.editingTextIndex === index) {
1122
- this.cancelEditing();
1123
- }
1124
- else if (this.editingTextIndex !== null && this.editingTextIndex > index) {
1125
- // 调整编辑索引
1126
- this.editingTextIndex--;
1127
- }
1128
- return true;
1129
- }
1130
- /**
1131
- * 清除所有文本标注
1132
- */
1133
- clearTextAnnotations() {
1134
- this.textAnnotations = [];
1135
- this.selectedTextIndex = null;
1136
- this.cancelEditing();
1137
- // 清空删除历史
1138
- this.deleteHistory = [];
1139
- }
1140
- /**
1141
- * 开始编辑文本标注
1142
- */
1143
- startEditing(index) {
1144
- if (!this.textInput || index < 0 || index >= this.textAnnotations.length) {
1145
- return false;
1146
- }
1147
- const textData = this.textAnnotations[index];
1148
- this.editingTextIndex = index;
1149
- this.currentEditingIndex = index;
1150
- this.textBeforeEditing = textData.text;
1151
- // 计算画布位置
1152
- const canvasX = this.viewport.offset.x + textData.position.x * this.viewport.scale;
1153
- const canvasY = this.viewport.offset.y + textData.position.y * this.viewport.scale;
1154
- // 定位输入框
1155
- this.textInput.value = textData.text;
1156
- this.textInput.style.left = `${canvasX - this.textStyle.padding}px`;
1157
- this.textInput.style.top = `${canvasY - this.textStyle.padding}px`;
1158
- this.textInput.style.display = "block";
1159
- // 调整输入框宽度
1160
- this.adjustInputWidth();
1161
- // 延迟聚焦
1162
- setTimeout(() => {
1163
- if (this.textInput) {
1164
- this.textInput.focus();
1165
- this.textInput.select();
1166
- }
1167
- }, 10);
1168
- return true;
1169
- }
1170
- /**
1171
- * 完成编辑
1172
- */
1173
- finishEditing() {
1174
- if (!this.textInput)
1175
- return false;
1176
- const index = this.currentEditingIndex;
1177
- const newText = this.textInput.value.trim();
1178
- if (index === null || index < 0 || index >= this.textAnnotations.length) {
1179
- this.resetEditingState();
1180
- return false;
1181
- }
1182
- if (newText === "") {
1183
- // 如果文本为空,删除该标注
1184
- this.textAnnotations.splice(index, 1);
1185
- }
1186
- else {
1187
- const textData = this.textAnnotations[index];
1188
- textData.text = newText;
1189
- // 更新文本尺寸
1190
- this.ctx.font = this.textStyle.font;
1191
- const metrics = this.ctx.measureText(textData.text);
1192
- textData.width = metrics.width;
1193
- textData.height = parseInt(this.textStyle.font) * 1.2;
1194
- }
1195
- this.resetEditingState();
1196
- return true;
1197
- }
1198
- /**
1199
- * 取消编辑
1200
- */
1201
- cancelEditing() {
1202
- if (!this.textInput || this.editingTextIndex === null)
1203
- return false;
1204
- // 恢复原文本
1205
- this.textInput.value = this.textBeforeEditing;
1206
- // 如果原文本为空且这是新创建的标注,删除它
1207
- const index = this.editingTextIndex;
1208
- if (index >= 0 && index < this.textAnnotations.length) {
1209
- const textData = this.textAnnotations[index];
1210
- if (textData.text === "" && this.textBeforeEditing === "") {
1211
- this.textAnnotations.splice(index, 1);
1212
- }
1213
- }
1214
- this.resetEditingState();
1215
- return true;
1216
- }
1217
- /**
1218
- * 删除正在编辑的文本标注
1219
- */
1220
- deleteEditingAnnotation() {
1221
- if (this.editingTextIndex === null)
1222
- return false;
1223
- const index = this.editingTextIndex;
1224
- // 隐藏输入框
1225
- if (this.textInput) {
1226
- this.textInput.style.display = "none";
1227
- }
1228
- // 删除标注
1229
- if (index >= 0 && index < this.textAnnotations.length) {
1230
- this.textAnnotations.splice(index, 1);
1231
- }
1232
- this.resetEditingState();
1233
- return true;
1234
- }
1235
- /**
1236
- * 重置编辑状态
1237
- */
1238
- resetEditingState() {
1239
- if (this.textInput) {
1240
- this.textInput.style.display = "none";
1241
- }
1242
- this.editingTextIndex = null;
1243
- this.currentEditingIndex = null;
1244
- this.textBeforeEditing = "";
1245
- }
1246
- /**
1247
- * 调整输入框宽度
1248
- */
1249
- adjustInputWidth() {
1250
- if (!this.textInput || this.editingTextIndex === null)
1251
- return;
1252
- this.ctx.font = this.textStyle.font;
1253
- const text = this.textInput.value || this.textInput.placeholder || "";
1254
- const metrics = this.ctx.measureText(text);
1255
- const textWidth = metrics.width;
1256
- const minWidth = 60;
1257
- const maxWidth = 200;
1258
- const newWidth = Math.min(Math.max(textWidth + 24, minWidth), maxWidth);
1259
- this.textInput.style.width = `${newWidth}px`;
1260
- }
1261
- /**
1262
- * 开始移动文本标注
1263
- */
1264
- startMoving(e, index) {
1265
- if (index < 0 || index >= this.textAnnotations.length)
1266
- return false;
1267
- this.editingTextIndex = index;
1268
- this.isTextMoving = true;
1269
- this.textMoveStart = { x: e.clientX, y: e.clientY };
1270
- return true;
1271
- }
1272
- /**
1273
- * 移动文本标注
1274
- */
1275
- moveAnnotation(e) {
1276
- if (!this.isTextMoving || this.editingTextIndex === null)
1277
- return false;
1278
- const dx = e.clientX - this.textMoveStart.x;
1279
- const dy = e.clientY - this.textMoveStart.y;
1280
- const textData = this.textAnnotations[this.editingTextIndex];
1281
- if (textData) {
1282
- textData.position.x += dx / this.viewport.scale;
1283
- textData.position.y += dy / this.viewport.scale;
1284
- this.textMoveStart = { x: e.clientX, y: e.clientY };
1285
- return true;
1286
- }
1287
- return false;
1288
- }
1289
- /**
1290
- * 完成移动
1291
- */
1292
- finishMoving() {
1293
- this.isTextMoving = false;
1294
- }
1295
- /**
1296
- * 检查是否点击了文本标注
1297
- */
1298
- checkTextClick(offsetX, offsetY) {
1299
- // 从后往前遍历,优先选中上层的文本
1300
- for (let i = this.textAnnotations.length - 1; i >= 0; i--) {
1301
- const textData = this.textAnnotations[i];
1302
- const canvasX = this.viewport.offset.x + textData.position.x * this.viewport.scale;
1303
- const canvasY = this.viewport.offset.y + textData.position.y * this.viewport.scale;
1304
- if (offsetX >= canvasX - this.textStyle.padding &&
1305
- offsetX <= canvasX + textData.width + this.textStyle.padding &&
1306
- offsetY >= canvasY - this.textStyle.padding &&
1307
- offsetY <= canvasY + textData.height + this.textStyle.padding) {
1308
- return i;
1309
- }
1310
- }
1311
- return null;
1312
- }
1313
- /**
1314
- * 检查是否点击了文本标注(用于移动)
1315
- */
1316
- checkTextClickForMove(e) {
1317
- const index = this.checkTextClick(e.offsetX, e.offsetY);
1318
- if (index !== null) {
1319
- this.startMoving(e, index);
1320
- return { index, handled: true };
1321
- }
1322
- return { index: -1, handled: false };
1323
- }
1324
- /**
1325
- * 获取所有文本标注
1326
- */
1327
- getTextAnnotations() {
1328
- return [...this.textAnnotations];
1329
- }
1330
- /**
1331
- * 撤销文本标注
1332
- */
1333
- withdraw() {
1334
- // 优先恢复删除的文本标注
1335
- if (this.deleteHistory.length > 0) {
1336
- const { annotation, index } = this.deleteHistory.pop();
1337
- // 在原来的位置插入
1338
- this.textAnnotations.splice(index, 0, annotation);
1339
- return true;
1340
- }
1341
- // 否则删除最后一个文本标注
1342
- if (this.textAnnotations.length > 0) {
1343
- this.textAnnotations.pop();
1344
- return true;
1345
- }
1346
- return false;
1347
- }
1348
- /**
1349
- * 选中文本标注
1350
- */
1351
- selectTextAnnotation(index) {
1352
- if (index < 0 || index >= this.textAnnotations.length) {
1353
- this.deselectTextAnnotation();
1354
- return false;
1355
- }
1356
- this.selectedTextIndex = index;
1357
- // 取消编辑状态
1358
- if (this.editingTextIndex !== null) {
1359
- this.finishEditing();
1360
- }
1361
- return true;
1362
- }
1363
- /**
1364
- * 取消选中文本标注
1365
- */
1366
- deselectTextAnnotation() {
1367
- this.selectedTextIndex = null;
1368
- }
1369
- /**
1370
- * 删除选中的文本标注
1371
- */
1372
- deleteSelectedTextAnnotation() {
1373
- if (this.selectedTextIndex === null)
1374
- return false;
1375
- const index = this.selectedTextIndex;
1376
- const annotation = this.textAnnotations[index];
1377
- // 保存删除的记录到历史(用于撤销)
1378
- this.deleteHistory.push({
1379
- annotation: Object.assign({}, annotation),
1380
- index
1381
- });
1382
- this.textAnnotations.splice(index, 1);
1383
- this.selectedTextIndex = null;
1384
- // 调整编辑索引
1385
- if (this.editingTextIndex !== null && this.editingTextIndex > index) {
1386
- this.editingTextIndex--;
1387
- }
1388
- return true;
1389
- }
1390
- /**
1391
- * 获取选中的文本标注信息
1392
- */
1393
- getSelectedTextAnnotation() {
1394
- if (this.selectedTextIndex === null)
1395
- return null;
1396
- const annotation = this.textAnnotations[this.selectedTextIndex];
1397
- if (!annotation)
1398
- return null;
1399
- return {
1400
- index: this.selectedTextIndex,
1401
- data: annotation
1402
- };
1403
- }
1404
- /**
1405
- * 销毁(清理 DOM)
1406
- */
1407
- destroy() {
1408
- if (this.textInput && this.textInput.parentNode) {
1409
- this.textInput.parentNode.removeChild(this.textInput);
1410
- }
1411
- }
1412
- }
1413
-
1414
- /**
1415
- * 渲染模块
1416
- * 负责所有绘制操作,包括背景图片、标注、选中高亮、文本标注等
1417
- */
1418
- class Renderer {
1419
- constructor(ctx, viewport, annotationManager, textManager, canvas) {
1420
- this.ctx = ctx;
1421
- this.viewport = viewport;
1422
- this.annotationManager = annotationManager;
1423
- this.textManager = textManager;
1424
- this.canvas = canvas;
1425
- }
1426
- /**
1427
- * 主渲染方法
1428
- */
1429
- render(bgImage) {
1430
- // 清除画布
1431
- this.ctx.clearRect(0, 0, this.viewport.width, this.viewport.height);
1432
- // 绘制背景图片
1433
- if (bgImage) {
1434
- this.drawBackgroundImage(bgImage);
1435
- }
1436
- // 绘制标注
1437
- this.drawAnnotations();
1438
- // 绘制文本标注
1439
- this.drawTextAnnotations();
1440
- }
1441
- /**
1442
- * 绘制背景图片
1443
- */
1444
- drawBackgroundImage(bgImage) {
1445
- this.ctx.drawImage(bgImage, this.viewport.offset.x, this.viewport.offset.y, this.viewport.originalWidth * this.viewport.scale, this.viewport.originalHeight * this.viewport.scale);
1446
- }
1447
- /**
1448
- * 绘制标注(矩形、多边形)
1449
- */
1450
- drawAnnotations() {
1451
- // 绘制已完成标注 - 每个标注始终使用自己的样式
1452
- this.annotationManager.recordList.forEach((item, index) => {
1453
- var _a;
1454
- // 获取标注保存的样式
1455
- const style = this.annotationManager.getAnnotationStyle(item);
1456
- // 始终使用标注自己的样式(选中也不改变样式)
1457
- this.ctx.strokeStyle = style.strokeColor;
1458
- this.ctx.lineWidth = style.lineWidth;
1459
- this.setLineDash(style.lineStyle || 'solid');
1460
- this.ctx.fillStyle = this.ctx.strokeStyle;
1461
- const isSelected = ((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.index) === index;
1462
- if (item.type === "rect") {
1463
- this.drawRect(item.data[0], isSelected);
1464
- }
1465
- else if (item.type === "polygon") {
1466
- // 使用标注保存的顶点样式
1467
- const vertexStyle = style.vertexStyle || this.annotationManager.getVertexStyle();
1468
- this.drawPolygon(item.data, item.status === "fullfilled", vertexStyle);
1469
- }
1470
- });
1471
- // 绘制当前操作(正在绘制的标注)- 使用当前设置的新样式
1472
- if (this.annotationManager.operate.data.length > 0) {
1473
- this.ctx.strokeStyle = this.annotationManager.strokeStyle;
1474
- this.ctx.lineWidth = this.annotationManager.lineWidth;
1475
- this.ctx.fillStyle = this.ctx.strokeStyle;
1476
- this.setLineDash(this.annotationManager.lineStyle);
1477
- if (this.annotationManager.operate.type === "rect") {
1478
- this.drawRect(this.annotationManager.operate.data[0], false);
1479
- }
1480
- else if (this.annotationManager.operate.type === "polygon") {
1481
- this.drawCurrentPolygon();
1482
- }
1483
- }
1484
- // 重置虚线设置
1485
- this.ctx.setLineDash([]);
1486
- // 绘制选中高亮
1487
- this.drawSelectionHighlight();
1488
- }
1489
- /**
1490
- * 设置边线虚线样式
1491
- */
1492
- setLineDash(style) {
1493
- switch (style) {
1494
- case 'dashed':
1495
- this.ctx.setLineDash([10, 5]);
1496
- break;
1497
- case 'dotted':
1498
- this.ctx.setLineDash([2, 4]);
1499
- break;
1500
- case 'solid':
1501
- default:
1502
- this.ctx.setLineDash([]);
1503
- break;
1504
- }
1505
- }
1506
- /**
1507
- * 绘制矩形
1508
- * @param rect - 矩形数据
1509
- * @param isSelected - 是否被选中(用于顶点颜色)
1510
- */
1511
- drawRect(rect, _isSelected = false) {
1512
- this.ctx.strokeRect(this.viewport.offset.x + rect.start.x * this.viewport.scale, this.viewport.offset.y + rect.start.y * this.viewport.scale, rect.width * this.viewport.scale, rect.height * this.viewport.scale);
1513
- }
1514
- /**
1515
- * 绘制多边形
1516
- * @param polygon - 多边形顶点数据
1517
- * @param closed - 是否闭合
1518
- * @param vertexStyle - 顶点样式
1519
- */
1520
- drawPolygon(polygon, closed, vertexStyle) {
1521
- if (polygon.length === 0)
1522
- return;
1523
- this.ctx.beginPath();
1524
- polygon.forEach((point, index) => {
1525
- const x = this.viewport.offset.x + point.point.x * this.viewport.scale;
1526
- const y = this.viewport.offset.y + point.point.y * this.viewport.scale;
1527
- if (index === 0) {
1528
- this.ctx.moveTo(x, y);
1529
- }
1530
- else {
1531
- this.ctx.lineTo(x, y);
1532
- }
1533
- });
1534
- if (closed && polygon.length > 1) {
1535
- const first = polygon[0];
1536
- this.ctx.lineTo(this.viewport.offset.x + first.point.x * this.viewport.scale, this.viewport.offset.y + first.point.y * this.viewport.scale);
1537
- }
1538
- this.ctx.stroke();
1539
- // 绘制顶点标记
1540
- this.drawPolygonVertices(polygon, vertexStyle);
1541
- }
1542
- /**
1543
- * 绘制多边形顶点标记
1544
- * @param polygon - 多边形顶点数据
1545
- * @param vertexStyle - 顶点样式
1546
- */
1547
- drawPolygonVertices(polygon, vertexStyle) {
1548
- // 使用传入的样式或当前全局样式
1549
- const style = vertexStyle || this.annotationManager.getVertexStyle();
1550
- const size = style.size / 2;
1551
- polygon.forEach((point) => {
1552
- const x = this.viewport.offset.x + point.point.x * this.viewport.scale;
1553
- const y = this.viewport.offset.y + point.point.y * this.viewport.scale;
1554
- this.ctx.fillStyle = style.fillColor;
1555
- this.ctx.strokeStyle = style.strokeColor;
1556
- this.ctx.lineWidth = style.strokeWidth;
1557
- this.ctx.beginPath();
1558
- switch (style.shape) {
1559
- case 'square':
1560
- this.ctx.rect(x - size, y - size, size * 2, size * 2);
1561
- break;
1562
- case 'diamond':
1563
- this.ctx.moveTo(x, y - size);
1564
- this.ctx.lineTo(x + size, y);
1565
- this.ctx.lineTo(x, y + size);
1566
- this.ctx.lineTo(x - size, y);
1567
- this.ctx.closePath();
1568
- break;
1569
- case 'circle':
1570
- default:
1571
- this.ctx.arc(x, y, size, 0, Math.PI * 2);
1572
- break;
1573
- }
1574
- this.ctx.fill();
1575
- if (style.strokeWidth > 0) {
1576
- this.ctx.stroke();
1577
- }
1578
- });
1579
- }
1580
- /**
1581
- * 绘制当前正在绘制的多边形
1582
- */
1583
- drawCurrentPolygon() {
1584
- const data = this.annotationManager.operate.data;
1585
- const tempPoint = this.annotationManager.tempPolygonPoint;
1586
- this.ctx.beginPath();
1587
- data.forEach((point, index) => {
1588
- const x = this.viewport.offset.x + point.point.x * this.viewport.scale;
1589
- const y = this.viewport.offset.y + point.point.y * this.viewport.scale;
1590
- if (index === 0) {
1591
- this.ctx.moveTo(x, y);
1592
- }
1593
- else {
1594
- this.ctx.lineTo(x, y);
1595
- }
1596
- });
1597
- // 绘制到临时点
1598
- if (tempPoint) {
1599
- const x = this.viewport.offset.x + tempPoint.x * this.viewport.scale;
1600
- const y = this.viewport.offset.y + tempPoint.y * this.viewport.scale;
1601
- this.ctx.lineTo(x, y);
1602
- }
1603
- this.ctx.stroke();
1604
- // 绘制顶点标记 - 使用当前设置的顶点样式
1605
- this.drawPolygonVertices(data, this.annotationManager.getVertexStyle());
1606
- // 绘制临时点标记
1607
- if (tempPoint) {
1608
- const vertexStyle = this.annotationManager.getVertexStyle();
1609
- const x = this.viewport.offset.x + tempPoint.x * this.viewport.scale;
1610
- const y = this.viewport.offset.y + tempPoint.y * this.viewport.scale;
1611
- const size = vertexStyle.size / 2;
1612
- this.ctx.fillStyle = vertexStyle.fillColor;
1613
- this.ctx.strokeStyle = vertexStyle.strokeColor;
1614
- this.ctx.lineWidth = vertexStyle.strokeWidth;
1615
- this.ctx.beginPath();
1616
- switch (vertexStyle.shape) {
1617
- case 'square':
1618
- this.ctx.rect(x - size, y - size, size * 2, size * 2);
1619
- break;
1620
- case 'diamond':
1621
- this.ctx.moveTo(x, y - size);
1622
- this.ctx.lineTo(x + size, y);
1623
- this.ctx.lineTo(x, y + size);
1624
- this.ctx.lineTo(x - size, y);
1625
- this.ctx.closePath();
1626
- break;
1627
- case 'circle':
1628
- default:
1629
- this.ctx.arc(x, y, size, 0, Math.PI * 2);
1630
- break;
1631
- }
1632
- this.ctx.fill();
1633
- if (vertexStyle.strokeWidth > 0) {
1634
- this.ctx.stroke();
1635
- }
1636
- }
1637
- }
1638
- /**
1639
- * 绘制选中状态的标注高亮
1640
- */
1641
- drawSelectionHighlight() {
1642
- const selected = this.annotationManager.selectedAnnotation;
1643
- if (!selected)
1644
- return;
1645
- const annotation = this.annotationManager.recordList[selected.index];
1646
- if (!annotation)
1647
- return;
1648
- const style = this.annotationManager.selectionStyle;
1649
- this.ctx.save();
1650
- if (annotation.type === "rect") {
1651
- this.drawRectSelectionHighlight(annotation.data[0], style);
1652
- }
1653
- else if (annotation.type === "polygon") {
1654
- this.drawPolygonSelectionHighlight(annotation.data, style);
1655
- }
1656
- this.ctx.restore();
1657
- }
1658
- /**
1659
- * 绘制矩形选中高亮
1660
- */
1661
- drawRectSelectionHighlight(rect, style) {
1662
- const x = this.viewport.offset.x + rect.start.x * this.viewport.scale;
1663
- const y = this.viewport.offset.y + rect.start.y * this.viewport.scale;
1664
- const w = rect.width * this.viewport.scale;
1665
- const h = rect.height * this.viewport.scale;
1666
- // 绘制半透明填充
1667
- this.ctx.fillStyle = style.fillColor;
1668
- this.ctx.fillRect(x, y, w, h);
1669
- // 绘制选中边框
1670
- this.ctx.strokeStyle = style.strokeColor;
1671
- this.ctx.lineWidth = 2;
1672
- this.ctx.setLineDash([5, 5]);
1673
- this.ctx.strokeRect(x, y, w, h);
1674
- // 绘制四个角的控制点
1675
- const handleSize = style.handleSize;
1676
- this.ctx.fillStyle = style.handleColor;
1677
- this.ctx.setLineDash([]);
1678
- // 左上
1679
- this.ctx.fillRect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
1680
- // 右上
1681
- this.ctx.fillRect(x + w - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
1682
- // 左下
1683
- this.ctx.fillRect(x - handleSize / 2, y + h - handleSize / 2, handleSize, handleSize);
1684
- // 右下
1685
- this.ctx.fillRect(x + w - handleSize / 2, y + h - handleSize / 2, handleSize, handleSize);
1686
- }
1687
- /**
1688
- * 绘制多边形选中高亮
1689
- */
1690
- drawPolygonSelectionHighlight(polygon, style) {
1691
- // 绘制半透明填充
1692
- this.ctx.beginPath();
1693
- polygon.forEach((p, i) => {
1694
- const x = this.viewport.offset.x + p.point.x * this.viewport.scale;
1695
- const y = this.viewport.offset.y + p.point.y * this.viewport.scale;
1696
- if (i === 0) {
1697
- this.ctx.moveTo(x, y);
1698
- }
1699
- else {
1700
- this.ctx.lineTo(x, y);
1701
- }
1702
- });
1703
- this.ctx.closePath();
1704
- this.ctx.fillStyle = style.fillColor;
1705
- this.ctx.fill();
1706
- // 绘制选中边框
1707
- this.ctx.strokeStyle = style.strokeColor;
1708
- this.ctx.lineWidth = 2;
1709
- this.ctx.setLineDash([5, 5]);
1710
- this.ctx.stroke();
1711
- // 绘制顶点控制点
1712
- const handleSize = style.handleSize;
1713
- this.ctx.fillStyle = style.handleColor;
1714
- this.ctx.setLineDash([]);
1715
- polygon.forEach((p) => {
1716
- const x = this.viewport.offset.x + p.point.x * this.viewport.scale;
1717
- const y = this.viewport.offset.y + p.point.y * this.viewport.scale;
1718
- this.ctx.fillRect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
1719
- });
1720
- }
1721
- /**
1722
- * 绘制文本标注
1723
- */
1724
- drawTextAnnotations() {
1725
- const globalStyle = this.textManager.textStyle;
1726
- this.textManager.textAnnotations.forEach((textData, index) => {
1727
- const isEditing = this.textManager.editingTextIndex === index;
1728
- // 如果正在编辑(输入框显示中),不在 canvas 上绘制文本
1729
- if (isEditing && this.textManager.textInput && this.textManager.textInput.style.display !== "none") {
1730
- return;
1731
- }
1732
- // 空文本不绘制
1733
- if (!textData.text)
1734
- return;
1735
- // 获取文本标注的样式(如果有保存的样式则使用,否则使用当前全局样式)
1736
- const textStyle = textData.style || {
1737
- font: globalStyle.font,
1738
- color: globalStyle.color,
1739
- backgroundColor: globalStyle.backgroundColor
1740
- };
1741
- const canvasX = this.viewport.offset.x + textData.position.x * this.viewport.scale;
1742
- // canvasY 是文本基线位置(fillText 的 y 参数)
1743
- const canvasY = this.viewport.offset.y + textData.position.y * this.viewport.scale;
1744
- // 检查是否被选中
1745
- const isSelected = this.textManager.selectedTextIndex === index;
1746
- // 设置字体以获取准确的文本度量
1747
- this.ctx.font = textStyle.font;
1748
- const textMetrics = this.ctx.measureText(textData.text);
1749
- // 计算文本实际高度(ascent + descent)
1750
- const ascent = textMetrics.actualBoundingBoxAscent || textData.height * 0.8;
1751
- const descent = textMetrics.actualBoundingBoxDescent || textData.height * 0.2;
1752
- const actualTextHeight = ascent + descent;
1753
- // 计算背景框尺寸
1754
- const r = globalStyle.borderRadius;
1755
- const bgX = canvasX - globalStyle.padding;
1756
- const boxWidth = textData.width + globalStyle.padding * 2;
1757
- const boxHeight = actualTextHeight + globalStyle.padding * 2;
1758
- // 背景框的 Y 位置:
1759
- // canvasY 是基线位置,背景框顶部应该在基线位置上方 ascent + padding 处
1760
- const bgY = canvasY - ascent - globalStyle.padding;
1761
- // 绘制背景(带圆角)
1762
- this.ctx.fillStyle = textStyle.backgroundColor;
1763
- this.ctx.beginPath();
1764
- this.ctx.roundRect(bgX, bgY, boxWidth, boxHeight, r);
1765
- this.ctx.fill();
1766
- // 如果选中,绘制选中边框
1767
- if (isSelected) {
1768
- this.ctx.strokeStyle = globalStyle.selectedBorderColor;
1769
- this.ctx.lineWidth = 2;
1770
- this.ctx.setLineDash([5, 5]);
1771
- this.ctx.beginPath();
1772
- this.ctx.roundRect(bgX, bgY, boxWidth, boxHeight, r);
1773
- this.ctx.stroke();
1774
- this.ctx.setLineDash([]);
1775
- }
1776
- // 绘制文本(在基线位置)
1777
- this.ctx.fillStyle = textStyle.color;
1778
- this.ctx.fillText(textData.text, canvasX, canvasY);
1779
- });
1780
- }
1781
- /**
1782
- * 导出时绘制所有内容到指定上下文
1783
- */
1784
- drawForExport(ctx, bgImage, originalWidth, originalHeight) {
1785
- // 绘制背景图片
1786
- ctx.drawImage(bgImage, 0, 0, originalWidth, originalHeight);
1787
- // 绘制标注 - 每个标注使用自己保存的样式
1788
- this.annotationManager.recordList.forEach((item) => {
1789
- // 获取标注保存的样式
1790
- const style = this.annotationManager.getAnnotationStyle(item);
1791
- ctx.strokeStyle = style.strokeColor;
1792
- ctx.lineWidth = style.lineWidth;
1793
- if (item.type === "rect") {
1794
- const rect = item.data[0];
1795
- ctx.strokeRect(rect.start.x, rect.start.y, rect.width, rect.height);
1796
- }
1797
- else if (item.type === "polygon" && item.status === "fullfilled") {
1798
- ctx.beginPath();
1799
- item.data.forEach((point, index) => {
1800
- if (index === 0) {
1801
- ctx.moveTo(point.point.x, point.point.y);
1802
- }
1803
- else {
1804
- ctx.lineTo(point.point.x, point.point.y);
1805
- }
1806
- });
1807
- const first = item.data[0];
1808
- ctx.lineTo(first.point.x, first.point.y);
1809
- ctx.stroke();
1810
- item.data.forEach((point) => {
1811
- ctx.beginPath();
1812
- ctx.arc(point.point.x, point.point.y, 4, 0, Math.PI * 2);
1813
- ctx.fillStyle = style.strokeColor;
1814
- ctx.fill();
1815
- });
1816
- }
1817
- });
1818
- // 绘制文本标注 - 每个文本使用自己保存的样式
1819
- this.textManager.textAnnotations.forEach((textData) => {
1820
- const globalStyle = this.textManager.textStyle;
1821
- // 获取文本标注的样式(如果有保存的样式则使用,否则使用当前全局样式)
1822
- const textStyle = textData.style || {
1823
- font: globalStyle.font,
1824
- color: globalStyle.color,
1825
- backgroundColor: globalStyle.backgroundColor
1826
- };
1827
- ctx.font = textStyle.font;
1828
- // 测量文本以获取准确的高度信息
1829
- const textMetrics = ctx.measureText(textData.text);
1830
- const ascent = textMetrics.actualBoundingBoxAscent || textData.height * 0.8;
1831
- const descent = textMetrics.actualBoundingBoxDescent || textData.height * 0.2;
1832
- const actualTextHeight = ascent + descent;
1833
- // 计算背景框尺寸和位置
1834
- const bgX = textData.position.x - globalStyle.padding;
1835
- const bgWidth = textData.width + globalStyle.padding * 2;
1836
- const bgHeight = actualTextHeight + globalStyle.padding * 2;
1837
- // 背景框顶部位置 = 基线位置 - 上行高度 - 内边距
1838
- const bgY = textData.position.y - ascent - globalStyle.padding;
1839
- const r = globalStyle.borderRadius;
1840
- // 绘制背景
1841
- ctx.fillStyle = textStyle.backgroundColor;
1842
- if (ctx.roundRect) {
1843
- ctx.roundRect(bgX, bgY, bgWidth, bgHeight, r);
1844
- ctx.fill();
1845
- }
1846
- else {
1847
- ctx.fillRect(bgX, bgY, bgWidth, bgHeight);
1848
- }
1849
- // 绘制文本(在基线位置)
1850
- ctx.fillStyle = textStyle.color;
1851
- ctx.fillText(textData.text, textData.position.x, textData.position.y);
1852
- });
1853
- }
1854
- }
1855
-
1856
- /**
1857
- * 事件处理模块
1858
- * 负责所有用户交互事件的处理
1859
- */
1860
- // getZoomDelta is imported in index.ts
1861
- class EventHandler {
1862
- constructor(canvas, viewport, annotationManager, textManager, renderer, getDrawType, getBgImage, renderCallback, setDrawTypeCallback) {
1863
- this.canvas = canvas;
1864
- this.viewport = viewport;
1865
- this.annotationManager = annotationManager;
1866
- this.textManager = textManager;
1867
- this.renderer = renderer;
1868
- this.getDrawType = getDrawType;
1869
- this.getBgImage = getBgImage;
1870
- this.renderCallback = renderCallback;
1871
- this.setDrawTypeCallback = setDrawTypeCallback;
1872
- // 拖拽状态
1873
- this.isDragging = false;
1874
- this.dragStart = { x: 0, y: 0 };
1875
- // 防止移动/调整大小后触发 click 绘制
1876
- this.justFinishedMove = false;
1877
- // 防止取消选中后触发 click 绘制
1878
- this.justDeselected = false;
1879
- }
1880
- /**
1881
- * 处理滚轮缩放
1882
- */
1883
- handleWheel(e) {
1884
- e.preventDefault();
1885
- if (!this.getBgImage())
1886
- return;
1887
- const delta = e.deltaY > 0 ? -0.02 : 0.02;
1888
- const oldScale = this.viewport.scale;
1889
- const newScale = Math.max(this.viewport.minScale, Math.min(oldScale + delta, this.viewport.maxScale));
1890
- // 计算缩放中心(鼠标位置)
1891
- const rect = this.canvas.getBoundingClientRect();
1892
- const mouseX = e.clientX - rect.left;
1893
- const mouseY = e.clientY - rect.top;
1894
- // 计算缩放前鼠标点在图像中的位置
1895
- const imgX = (mouseX - this.viewport.offset.x) / oldScale;
1896
- const imgY = (mouseY - this.viewport.offset.y) / oldScale;
1897
- // 更新缩放比例
1898
- this.viewport.scale = newScale;
1899
- // 计算缩放后鼠标点应该的位置
1900
- const newMouseX = imgX * newScale + this.viewport.offset.x;
1901
- const newMouseY = imgY * newScale + this.viewport.offset.y;
1902
- // 调整偏移量以保持鼠标点不变
1903
- this.viewport.offset.x += mouseX - newMouseX;
1904
- this.viewport.offset.y += mouseY - newMouseY;
1905
- // 更新边界约束
1906
- this.viewport.calculateBoundaries();
1907
- this.viewport.constrainViewport();
1908
- // 缩放后不再是初始状态
1909
- this.viewport.isInitialScale = this.viewport.scale === this.viewport.initialScale;
1910
- this.renderCallback();
1911
- }
1912
- /**
1913
- * 处理鼠标按下
1914
- */
1915
- handleMouseDown(e) {
1916
- var _a, _b;
1917
- if (e.button !== 0)
1918
- return; // 只处理左键
1919
- const drawType = this.getDrawType();
1920
- const bgImage = this.getBgImage();
1921
- // 优先处理:如果有选中的标注,检查是否点击了控制点
1922
- if (this.annotationManager.selectedAnnotation && bgImage) {
1923
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
1924
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
1925
- if (handle) {
1926
- this.annotationManager.startResizing(handle, imgCoords);
1927
- this.renderCallback();
1928
- return;
1929
- }
1930
- }
1931
- // 文本标注模式
1932
- if (drawType === "text") {
1933
- // 先检查是否点击了已有文本(只选中,不直接移动)
1934
- const textIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
1935
- if (textIndex !== null) {
1936
- // 如果点击的是已选中的文本,开始移动
1937
- if (this.textManager.selectedTextIndex === textIndex) {
1938
- this.textManager.startMoving(e, textIndex);
1939
- }
1940
- else {
1941
- // 否则选中文本(取消其他选中)
1942
- this.annotationManager.deselectAnnotation();
1943
- this.textManager.selectTextAnnotation(textIndex);
1944
- }
1945
- this.renderCallback();
1946
- return;
1947
- }
1948
- // 文本模式下也可以选中其他标注
1949
- if (bgImage) {
1950
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
1951
- // 检查矩形
1952
- const rectClicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
1953
- if ((rectClicked === null || rectClicked === void 0 ? void 0 : rectClicked.type) === "rect") {
1954
- this.annotationManager.selectAnnotation(rectClicked.index);
1955
- this.annotationManager.startMovingAnnotation(e);
1956
- this.renderCallback();
1957
- return;
1958
- }
1959
- // 检查多边形
1960
- const polygonClicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
1961
- if ((polygonClicked === null || polygonClicked === void 0 ? void 0 : polygonClicked.type) === "polygon") {
1962
- this.annotationManager.selectAnnotation(polygonClicked.index);
1963
- this.annotationManager.startMovingAnnotation(e);
1964
- this.renderCallback();
1965
- return;
1966
- }
1967
- // 如果点击了空白处且有选中的标注,取消选中
1968
- if (this.annotationManager.selectedAnnotation) {
1969
- this.annotationManager.deselectAnnotation();
1970
- this.renderCallback();
1971
- return;
1972
- }
1973
- }
1974
- // 如果点击了空白处且有选中的文本标注,取消选中
1975
- if (this.textManager.selectedTextIndex !== null) {
1976
- this.textManager.deselectTextAnnotation();
1977
- this.renderCallback();
1978
- return;
1979
- }
1980
- // 没有点击任何标注,创建新文本
1981
- this.handleTextModeClick(e);
1982
- return;
1983
- }
1984
- // 其他模式需要背景图片
1985
- if (!bgImage)
1986
- return;
1987
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
1988
- if (drawType === "rect") {
1989
- // 矩形模式:优先检测是否点击了任何标注(可以选中并移动)
1990
- // 如果当前有选中的矩形,优先处理选中状态
1991
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "rect") {
1992
- // 检查是否点击了控制点
1993
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
1994
- if (handle) {
1995
- this.annotationManager.startResizing(handle, imgCoords);
1996
- this.renderCallback();
1997
- return;
1998
- }
1999
- // 检查是否点击到已有矩形(可能是另一个矩形)
2000
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2001
- if (clicked && clicked.type === "rect") {
2002
- this.annotationManager.selectAnnotation(clicked.index);
2003
- this.annotationManager.startMovingAnnotation(e);
2004
- this.renderCallback();
2005
- return;
2006
- }
2007
- // 点击空白处,取消选中,不开始新矩形
2008
- this.annotationManager.deselectAnnotation();
2009
- this.justDeselected = true;
2010
- this.renderCallback();
2011
- return;
2012
- }
2013
- // 没有选中的矩形,可以选中其他类型标注或开始新矩形
2014
- // 1. 先检查文本(只选中,不直接移动)
2015
- const textIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2016
- if (textIndex !== null) {
2017
- // 如果点击的是已选中的文本,开始移动
2018
- if (this.textManager.selectedTextIndex === textIndex) {
2019
- this.textManager.startMoving(e, textIndex);
2020
- }
2021
- else {
2022
- // 否则选中文本(取消其他选中)
2023
- this.annotationManager.deselectAnnotation();
2024
- this.textManager.selectTextAnnotation(textIndex);
2025
- }
2026
- this.renderCallback();
2027
- return;
2028
- }
2029
- // 2. 检查多边形
2030
- const polygonClicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2031
- if ((polygonClicked === null || polygonClicked === void 0 ? void 0 : polygonClicked.type) === "polygon") {
2032
- this.annotationManager.selectAnnotation(polygonClicked.index);
2033
- this.annotationManager.startMovingAnnotation(e);
2034
- this.renderCallback();
2035
- return;
2036
- }
2037
- // 3. 检查矩形(选中它)
2038
- const rectClicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2039
- if ((rectClicked === null || rectClicked === void 0 ? void 0 : rectClicked.type) === "rect") {
2040
- this.annotationManager.selectAnnotation(rectClicked.index);
2041
- this.annotationManager.startMovingAnnotation(e);
2042
- this.renderCallback();
2043
- return;
2044
- }
2045
- // 开始绘制新矩形
2046
- this.annotationManager.startRectDrawing(imgCoords);
2047
- }
2048
- else if (drawType === "polygon") {
2049
- // 多边形模式:优先检测是否点击了任何标注(可以选中并移动)
2050
- // 如果当前有选中的多边形,优先处理选中状态
2051
- if (((_b = this.annotationManager.selectedAnnotation) === null || _b === void 0 ? void 0 : _b.type) === "polygon") {
2052
- // 检查是否点击了顶点
2053
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2054
- if (handle && handle.type === "polygon-vertex") {
2055
- this.annotationManager.startResizing(handle, imgCoords);
2056
- this.renderCallback();
2057
- return;
2058
- }
2059
- // 检查是否点击到已有多边形(可能是另一个多边形)
2060
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2061
- if (clicked && clicked.type === "polygon") {
2062
- this.annotationManager.selectAnnotation(clicked.index);
2063
- this.annotationManager.startMovingAnnotation(e);
2064
- this.renderCallback();
2065
- return;
2066
- }
2067
- // 点击空白处,取消选中,不开始新多边形
2068
- this.annotationManager.deselectAnnotation();
2069
- this.justDeselected = true;
2070
- this.renderCallback();
2071
- return;
2072
- }
2073
- // 没有选中的多边形,可以选中其他类型标注或开始新多边形
2074
- // 1. 先检查文本(只选中,不直接移动)
2075
- const textIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2076
- if (textIndex !== null) {
2077
- // 如果点击的是已选中的文本,开始移动
2078
- if (this.textManager.selectedTextIndex === textIndex) {
2079
- this.textManager.startMoving(e, textIndex);
2080
- }
2081
- else {
2082
- // 否则选中文本(取消其他选中)
2083
- this.annotationManager.deselectAnnotation();
2084
- this.textManager.selectTextAnnotation(textIndex);
2085
- }
2086
- this.renderCallback();
2087
- return;
2088
- }
2089
- // 2. 检查矩形
2090
- const rectClicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2091
- if ((rectClicked === null || rectClicked === void 0 ? void 0 : rectClicked.type) === "rect") {
2092
- this.annotationManager.selectAnnotation(rectClicked.index);
2093
- this.annotationManager.startMovingAnnotation(e);
2094
- this.renderCallback();
2095
- return;
2096
- }
2097
- // 3. 检查多边形(选中它)
2098
- const polyClicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2099
- if ((polyClicked === null || polyClicked === void 0 ? void 0 : polyClicked.type) === "polygon") {
2100
- this.annotationManager.selectAnnotation(polyClicked.index);
2101
- this.annotationManager.startMovingAnnotation(e);
2102
- this.renderCallback();
2103
- return;
2104
- }
2105
- // 开始绘制新多边形
2106
- if (!this.annotationManager.isDrawing) {
2107
- this.annotationManager.startPolygonDrawing(imgCoords);
2108
- }
2109
- else if (this.checkPolygonPointValid(imgCoords)) {
2110
- this.annotationManager.addPolygonPoint(imgCoords);
2111
- }
2112
- this.annotationManager.updatePolygonTempPoint(imgCoords);
2113
- this.renderCallback();
2114
- }
2115
- else if (drawType === "drag") {
2116
- // 拖拽模式
2117
- if (this.annotationManager.selectedAnnotation) {
2118
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2119
- if (handle) {
2120
- this.annotationManager.startResizing(handle, imgCoords);
2121
- this.renderCallback();
2122
- return;
2123
- }
2124
- }
2125
- // 检查文本(只选中,不直接移动)
2126
- const textIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2127
- if (textIndex !== null) {
2128
- // 如果点击的是已选中的文本,开始移动
2129
- if (this.textManager.selectedTextIndex === textIndex) {
2130
- this.textManager.startMoving(e, textIndex);
2131
- }
2132
- else {
2133
- // 否则选中文本(取消其他选中)
2134
- this.annotationManager.deselectAnnotation();
2135
- this.textManager.selectTextAnnotation(textIndex);
2136
- }
2137
- this.renderCallback();
2138
- return;
2139
- }
2140
- // 检查矩形
2141
- const rectHandled = this.handleRectModeClick(e, imgCoords);
2142
- if (rectHandled)
2143
- return;
2144
- // 检查多边形
2145
- const polygonHandled = this.handlePolygonModeClick(e, imgCoords);
2146
- if (polygonHandled)
2147
- return;
2148
- // 开始拖拽视图
2149
- this.isDragging = true;
2150
- this.dragStart = { x: e.clientX, y: e.clientY };
2151
- this.canvas.style.cursor = "grabbing";
2152
- }
2153
- else if (drawType === "") {
2154
- // 无模式:可以选中和编辑所有类型的标注
2155
- this.handleNoModeClick(e, imgCoords);
2156
- }
2157
- }
2158
- /**
2159
- * 拖拽模式下检查文本点击
2160
- */
2161
- checkTextClickInDragMode(e) {
2162
- const result = this.textManager.checkTextClickForMove(e);
2163
- if (result.handled) {
2164
- this.canvas.style.cursor = "grabbing";
2165
- return true;
2166
- }
2167
- return false;
2168
- }
2169
- /**
2170
- * 文本模式下的点击处理
2171
- */
2172
- handleTextModeClick(e) {
2173
- // 检测是否点击了已有文本
2174
- const clickedIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2175
- if (clickedIndex !== null) {
2176
- this.textManager.startMoving(e, clickedIndex);
2177
- this.canvas.style.cursor = "grabbing";
2178
- this.renderCallback();
2179
- return true;
2180
- }
2181
- // 没有点击文本,创建新文本
2182
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2183
- this.textManager.addTextAnnotation(imgCoords.x, imgCoords.y);
2184
- return true;
2185
- }
2186
- /**
2187
- * 矩形模式下的点击处理
2188
- */
2189
- handleRectModeClick(e, imgCoords) {
2190
- var _a, _b;
2191
- // 如果有选中的矩形,先检查是否点击了控制点
2192
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "rect") {
2193
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2194
- if (handle) {
2195
- this.annotationManager.startResizing(handle, imgCoords);
2196
- this.renderCallback();
2197
- return true;
2198
- }
2199
- }
2200
- // 检测是否点击到已有矩形
2201
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2202
- if (clicked && clicked.type === "rect") {
2203
- this.annotationManager.selectAnnotation(clicked.index);
2204
- this.annotationManager.startMovingAnnotation(e);
2205
- this.renderCallback();
2206
- return true;
2207
- }
2208
- // 如果当前有选中的矩形,点击空白处取消选中并返回true(表示已处理)
2209
- if (((_b = this.annotationManager.selectedAnnotation) === null || _b === void 0 ? void 0 : _b.type) === "rect") {
2210
- this.annotationManager.deselectAnnotation();
2211
- this.renderCallback();
2212
- return true;
2213
- }
2214
- // 没有选中矩形且没有点击到矩形,返回false让调用方继续处理
2215
- return false;
2216
- }
2217
- /**
2218
- * 多边形模式下的点击处理
2219
- */
2220
- handlePolygonModeClick(e, imgCoords) {
2221
- var _a, _b;
2222
- // 如果有选中的多边形,先检查是否点击了顶点
2223
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "polygon") {
2224
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2225
- if (handle && handle.type === "polygon-vertex") {
2226
- this.annotationManager.startResizing(handle, imgCoords);
2227
- this.renderCallback();
2228
- return true;
2229
- }
2230
- }
2231
- // 检测是否点击到已有多边形
2232
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2233
- if (clicked && clicked.type === "polygon") {
2234
- this.annotationManager.selectAnnotation(clicked.index);
2235
- this.annotationManager.startMovingAnnotation(e);
2236
- this.renderCallback();
2237
- return true;
2238
- }
2239
- // 如果当前有选中的多边形,点击空白处取消选中并返回true(表示已处理)
2240
- if (((_b = this.annotationManager.selectedAnnotation) === null || _b === void 0 ? void 0 : _b.type) === "polygon") {
2241
- this.annotationManager.deselectAnnotation();
2242
- this.renderCallback();
2243
- return true;
2244
- }
2245
- // 没有选中多边形且没有点击到多边形,返回false让调用方继续处理
2246
- return false;
2247
- }
2248
- /**
2249
- * 纯移动检测:检测是否点击了矩形
2250
- */
2251
- handleRectModeClickForMove(e, imgCoords) {
2252
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2253
- if (clicked && clicked.type === "rect") {
2254
- this.annotationManager.selectAnnotation(clicked.index);
2255
- this.annotationManager.startMovingAnnotation(e);
2256
- this.renderCallback();
2257
- return true;
2258
- }
2259
- return false;
2260
- }
2261
- /**
2262
- * 纯移动检测:检测是否点击了多边形
2263
- */
2264
- handlePolygonModeClickForMove(e, imgCoords) {
2265
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2266
- if (clicked && clicked.type === "polygon") {
2267
- this.annotationManager.selectAnnotation(clicked.index);
2268
- this.annotationManager.startMovingAnnotation(e);
2269
- this.renderCallback();
2270
- return true;
2271
- }
2272
- return false;
2273
- }
2274
- /**
2275
- * 无模式下的点击处理
2276
- */
2277
- handleNoModeClick(e, imgCoords) {
2278
- // 先检查是否点击了文本
2279
- const textIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2280
- if (textIndex !== null) {
2281
- // 如果点击的是已选中的文本,开始移动
2282
- if (this.textManager.selectedTextIndex === textIndex) {
2283
- this.textManager.startMoving(e, textIndex);
2284
- }
2285
- else {
2286
- // 否则选中文本(取消其他选中)
2287
- this.annotationManager.deselectAnnotation();
2288
- this.textManager.selectTextAnnotation(textIndex);
2289
- }
2290
- this.renderCallback();
2291
- return;
2292
- }
2293
- // 取消文本选中
2294
- this.textManager.deselectTextAnnotation();
2295
- // 再检查是否点击了矩形/多边形
2296
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2297
- if (clicked) {
2298
- this.annotationManager.selectAnnotation(clicked.index);
2299
- this.annotationManager.startMovingAnnotation(e);
2300
- this.renderCallback();
2301
- }
2302
- else {
2303
- this.annotationManager.deselectAnnotation();
2304
- }
2305
- }
2306
- /**
2307
- * 检查多边形点是否有效(避免重复点)
2308
- */
2309
- checkPolygonPointValid(imgCoords) {
2310
- const lastPoint = this.annotationManager.operate.data.length > 0
2311
- ? this.annotationManager.operate.data[this.annotationManager.operate.data.length - 1]
2312
- : null;
2313
- return (!lastPoint ||
2314
- Math.abs(lastPoint.point.x - imgCoords.x) > 5 ||
2315
- Math.abs(lastPoint.point.y - imgCoords.y) > 5);
2316
- }
2317
- /**
2318
- * 处理鼠标移动
2319
- */
2320
- handleMouseMove(e) {
2321
- if (!this.getBgImage())
2322
- return;
2323
- const drawType = this.getDrawType();
2324
- // 处理视图拖拽
2325
- if (this.isDragging && drawType === "drag") {
2326
- const dx = e.clientX - this.dragStart.x;
2327
- const dy = e.clientY - this.dragStart.y;
2328
- this.viewport.updateOffset(dx, dy);
2329
- this.dragStart = { x: e.clientX, y: e.clientY };
2330
- this.renderCallback();
2331
- return;
2332
- }
2333
- // 处理文本移动
2334
- if (this.textManager.isTextMoving && this.textManager.editingTextIndex !== null) {
2335
- if (this.textManager.moveAnnotation(e)) {
2336
- this.renderCallback();
2337
- }
2338
- return;
2339
- }
2340
- // 处理标注移动或调整大小
2341
- if ((this.annotationManager.isMovingAnnotation || this.annotationManager.isResizing) &&
2342
- this.annotationManager.selectedAnnotation) {
2343
- this.handleAnnotationMove(e);
2344
- return;
2345
- }
2346
- // 更新光标样式
2347
- if (!this.annotationManager.isMovingAnnotation &&
2348
- !this.annotationManager.isResizing &&
2349
- !this.textManager.isTextMoving) {
2350
- this.updateCursor(e);
2351
- }
2352
- // 处理矩形绘制
2353
- if (this.annotationManager.isDrawing && this.annotationManager.operate.type === "rect") {
2354
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2355
- this.annotationManager.updateRectDrawing(imgCoords);
2356
- this.renderCallback();
2357
- }
2358
- // 处理多边形临时点
2359
- if (drawType === "polygon" && this.annotationManager.isDrawing) {
2360
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2361
- this.annotationManager.updatePolygonTempPoint(imgCoords);
2362
- this.renderCallback();
2363
- }
2364
- }
2365
- /**
2366
- * 处理标注移动或调整大小
2367
- */
2368
- handleAnnotationMove(e) {
2369
- var _a, _b;
2370
- // 处理调整大小
2371
- if (this.annotationManager.isResizing && this.annotationManager.activeHandle) {
2372
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2373
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "rect") {
2374
- this.annotationManager.resizeRect(imgCoords);
2375
- }
2376
- else if (((_b = this.annotationManager.selectedAnnotation) === null || _b === void 0 ? void 0 : _b.type) === "polygon") {
2377
- this.annotationManager.resizePolygon(imgCoords);
2378
- }
2379
- this.renderCallback();
2380
- return;
2381
- }
2382
- // 处理移动
2383
- if (!this.annotationManager.isMovingAnnotation || !this.annotationManager.selectedAnnotation)
2384
- return;
2385
- const dx = (e.clientX - this.annotationManager.annotationMoveStart.x) / this.viewport.scale;
2386
- const dy = (e.clientY - this.annotationManager.annotationMoveStart.y) / this.viewport.scale;
2387
- this.annotationManager.moveSelectedAnnotation(dx, dy);
2388
- this.annotationManager.annotationMoveStart = { x: e.clientX, y: e.clientY };
2389
- this.renderCallback();
2390
- }
2391
- /**
2392
- * 更新鼠标光标样式
2393
- */
2394
- updateCursor(e) {
2395
- const drawType = this.getDrawType();
2396
- // 文本模式
2397
- if (drawType === "text") {
2398
- // 文本模式下也可以悬停其他标注
2399
- if (this.getBgImage()) {
2400
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2401
- // 检查控制点
2402
- if (this.annotationManager.selectedAnnotation) {
2403
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2404
- if (handle) {
2405
- if (handle.type === "rect-corner") {
2406
- this.canvas.style.cursor = handle.index === 0 || handle.index === 3 ? "nwse-resize" : "nesw-resize";
2407
- }
2408
- else {
2409
- this.canvas.style.cursor = "crosshair";
2410
- }
2411
- return;
2412
- }
2413
- }
2414
- // 检查矩形/多边形
2415
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2416
- if (clicked) {
2417
- this.canvas.style.cursor = "move";
2418
- return;
2419
- }
2420
- }
2421
- // 检查文本
2422
- const clickedIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2423
- this.canvas.style.cursor = clickedIndex !== null ? "move" : "crosshair";
2424
- return;
2425
- }
2426
- // 矩形模式
2427
- if (drawType === "rect") {
2428
- this.updateRectModeCursor(e);
2429
- return;
2430
- }
2431
- // 多边形模式
2432
- if (drawType === "polygon") {
2433
- this.updatePolygonModeCursor(e);
2434
- return;
2435
- }
2436
- // 拖拽模式
2437
- if (drawType === "drag") {
2438
- this.updateDragModeCursor(e);
2439
- return;
2440
- }
2441
- // 无模式
2442
- if (drawType === "") {
2443
- this.updateNoModeCursor(e);
2444
- return;
2445
- }
2446
- this.canvas.style.cursor = "default";
2447
- }
2448
- /**
2449
- * 更新矩形模式光标
2450
- */
2451
- updateRectModeCursor(e) {
2452
- var _a;
2453
- // 先检查控制点
2454
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "rect") {
2455
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2456
- if (handle) {
2457
- this.canvas.style.cursor = handle.index === 0 || handle.index === 3 ? "nwse-resize" : "nesw-resize";
2458
- return;
2459
- }
2460
- }
2461
- // 检查是否在矩形上
2462
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2463
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2464
- if (clicked) {
2465
- this.canvas.style.cursor = "move";
2466
- return;
2467
- }
2468
- // 检查文本
2469
- if (this.textManager.checkTextClick(e.offsetX, e.offsetY) !== null) {
2470
- this.canvas.style.cursor = "move";
2471
- return;
2472
- }
2473
- this.canvas.style.cursor = "crosshair";
2474
- }
2475
- /**
2476
- * 更新多边形模式光标
2477
- */
2478
- updatePolygonModeCursor(e) {
2479
- var _a;
2480
- // 先检查顶点
2481
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "polygon") {
2482
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2483
- if (handle) {
2484
- this.canvas.style.cursor = "crosshair";
2485
- return;
2486
- }
2487
- }
2488
- // 检查是否在多边形上
2489
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2490
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2491
- if (clicked) {
2492
- this.canvas.style.cursor = "move";
2493
- return;
2494
- }
2495
- // 检查文本
2496
- if (this.textManager.checkTextClick(e.offsetX, e.offsetY) !== null) {
2497
- this.canvas.style.cursor = "move";
2498
- return;
2499
- }
2500
- this.canvas.style.cursor = "crosshair";
2501
- }
2502
- /**
2503
- * 更新拖拽模式光标
2504
- */
2505
- updateDragModeCursor(e) {
2506
- // 检查控制点
2507
- if (this.annotationManager.selectedAnnotation) {
2508
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2509
- if (handle) {
2510
- if (handle.type === "rect-corner") {
2511
- this.canvas.style.cursor = handle.index === 0 || handle.index === 3 ? "nwse-resize" : "nesw-resize";
2512
- }
2513
- else {
2514
- this.canvas.style.cursor = "crosshair";
2515
- }
2516
- return;
2517
- }
2518
- }
2519
- // 检查文本
2520
- if (this.textManager.checkTextClick(e.offsetX, e.offsetY) !== null) {
2521
- this.canvas.style.cursor = "move";
2522
- return;
2523
- }
2524
- // 检查矩形/多边形
2525
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2526
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2527
- this.canvas.style.cursor = clicked ? "move" : "grab";
2528
- }
2529
- /**
2530
- * 更新无模式光标
2531
- */
2532
- updateNoModeCursor(e) {
2533
- // 检查控制点
2534
- if (this.annotationManager.selectedAnnotation) {
2535
- const handle = this.annotationManager.getHandleAtPoint(e.offsetX, e.offsetY);
2536
- if (handle) {
2537
- if (handle.type === "rect-corner") {
2538
- this.canvas.style.cursor = handle.index === 0 || handle.index === 3 ? "nwse-resize" : "nesw-resize";
2539
- }
2540
- else {
2541
- this.canvas.style.cursor = "crosshair";
2542
- }
2543
- return;
2544
- }
2545
- }
2546
- // 检查文本
2547
- if (this.textManager.checkTextClick(e.offsetX, e.offsetY) !== null) {
2548
- this.canvas.style.cursor = "move";
2549
- return;
2550
- }
2551
- // 检查矩形/多边形
2552
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2553
- const clicked = this.annotationManager.getAnnotationAtPoint(imgCoords);
2554
- this.canvas.style.cursor = clicked ? "move" : "default";
2555
- }
2556
- /**
2557
- * 处理鼠标释放
2558
- */
2559
- handleMouseUp(_e) {
2560
- if (this.isDragging) {
2561
- this.isDragging = false;
2562
- const drawType = this.getDrawType();
2563
- this.canvas.style.cursor = drawType === "drag" ? "grab" : "default";
2564
- return;
2565
- }
2566
- if (this.textManager.isTextMoving) {
2567
- this.justFinishedMove = true;
2568
- this.textManager.finishMoving();
2569
- const drawType = this.getDrawType();
2570
- if (drawType === "drag") {
2571
- this.canvas.style.cursor = "grab";
2572
- }
2573
- else if (drawType === "") {
2574
- this.canvas.style.cursor = "move";
2575
- }
2576
- else {
2577
- this.textManager.editingTextIndex = null;
2578
- this.canvas.style.cursor = "default";
2579
- }
2580
- }
2581
- // 结束标注移动或调整大小
2582
- if (this.annotationManager.isMovingAnnotation || this.annotationManager.isResizing) {
2583
- this.justFinishedMove = true;
2584
- this.annotationManager.finishMovingAnnotation();
2585
- this.canvas.style.cursor = this.annotationManager.selectedAnnotation ? "move" : "default";
2586
- }
2587
- // 完成矩形绘制
2588
- if (this.annotationManager.isDrawing && this.annotationManager.operate.type === "rect") {
2589
- this.annotationManager.finishRectDrawing();
2590
- this.renderCallback();
2591
- }
2592
- }
2593
- /**
2594
- * 处理鼠标离开
2595
- */
2596
- handleMouseLeave(e) {
2597
- if (this.isDragging || this.textManager.isTextMoving ||
2598
- this.annotationManager.isResizing || this.annotationManager.isMovingAnnotation) {
2599
- this.handleMouseUp(e);
2600
- }
2601
- // 不再自动取消选中状态
2602
- }
2603
- /**
2604
- * 处理鼠标双击
2605
- */
2606
- handleDoubleClick(e) {
2607
- if (!this.getBgImage())
2608
- return;
2609
- // 清除 justFinishedMove 标志
2610
- this.justFinishedMove = false;
2611
- // 优先检查是否双击了文本
2612
- const textIndex = this.textManager.checkTextClick(e.offsetX, e.offsetY);
2613
- if (textIndex !== null) {
2614
- this.textManager.startEditing(textIndex);
2615
- return;
2616
- }
2617
- // 多边形模式下双击结束绘制
2618
- const drawType = this.getDrawType();
2619
- if (drawType === "polygon") {
2620
- if (this.annotationManager.isDrawing && this.annotationManager.operate.data.length <= 2) {
2621
- return;
2622
- }
2623
- this.annotationManager.finishPolygonDrawing();
2624
- this.renderCallback();
2625
- }
2626
- }
2627
- /**
2628
- * 处理鼠标单击(用于多边形绘制)
2629
- */
2630
- handleClick(e) {
2631
- var _a;
2632
- const drawType = this.getDrawType();
2633
- if (!this.getBgImage() || drawType !== "polygon" ||
2634
- this.isDragging || this.annotationManager.isMovingAnnotation ||
2635
- this.annotationManager.isResizing || this.textManager.isTextMoving) {
2636
- return;
2637
- }
2638
- // 如果刚刚取消了选中,跳过绘制
2639
- if (this.justDeselected) {
2640
- this.justDeselected = false;
2641
- return;
2642
- }
2643
- // 如果当前有选中的多边形,不开始新多边形
2644
- if (((_a = this.annotationManager.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === "polygon") {
2645
- return;
2646
- }
2647
- const imgCoords = this.viewport.toImageCoordinates(e.offsetX, e.offsetY);
2648
- if (this.checkPolygonPointValid(imgCoords)) {
2649
- if (!this.annotationManager.isDrawing) {
2650
- this.annotationManager.startPolygonDrawing(imgCoords);
2651
- }
2652
- else {
2653
- this.annotationManager.addPolygonPoint(imgCoords);
2654
- }
2655
- }
2656
- this.annotationManager.updatePolygonTempPoint(imgCoords);
2657
- this.renderCallback();
2658
- }
2659
- /**
2660
- * 处理键盘按下
2661
- */
2662
- handleKeyDown(e) {
2663
- // 撤销操作
2664
- if (e.key === "z" && (e.ctrlKey || e.metaKey)) {
2665
- this.annotationManager.withdraw() || this.textManager.withdraw();
2666
- this.renderCallback();
2667
- return;
2668
- }
2669
- // 空格键重置视图
2670
- if (e.key === " ") {
2671
- this.viewport.resetToInitial();
2672
- this.renderCallback();
2673
- return;
2674
- }
2675
- // ESC键
2676
- if (e.key === "Escape") {
2677
- if (this.annotationManager.isDrawing) {
2678
- this.annotationManager.cancelDrawing();
2679
- this.renderCallback();
2680
- }
2681
- else if (this.annotationManager.selectedAnnotation) {
2682
- this.annotationManager.deselectAnnotation();
2683
- this.renderCallback();
2684
- }
2685
- return;
2686
- }
2687
- // Delete 键删除选中的标注
2688
- if (e.key === "Delete" || e.key === "Backspace") {
2689
- // 优先删除正在编辑的文本标注
2690
- if (this.textManager.editingTextIndex !== null) {
2691
- this.textManager.removeTextAnnotation(this.textManager.editingTextIndex);
2692
- this.renderCallback();
2693
- return;
2694
- }
2695
- // 删除选中的文本标注(非编辑状态)
2696
- if (this.textManager.selectedTextIndex !== null) {
2697
- this.textManager.deleteSelectedTextAnnotation();
2698
- this.renderCallback();
2699
- return;
2700
- }
2701
- // 删除选中的矩形/多边形标注
2702
- if (this.annotationManager.selectedAnnotation) {
2703
- this.annotationManager.deleteSelectedAnnotation();
2704
- this.renderCallback();
2705
- }
2706
- }
2707
- }
2708
- }
2709
-
2710
- /**
2711
- * Image Annotation Drawer
2712
- * 一个轻量级的图片标注库,基于 HTML5 Canvas
2713
- *
2714
- * 功能特性:
2715
- * - 多种标注类型:矩形、多边形、文本标注
2716
- * - 视图操作:鼠标滚轮缩放、拖拽平移
2717
- * - 撤销功能:支持撤销上一步操作
2718
- * - 快捷键:空格键重置视图,ESC 取消当前绘制
2719
- * - 图片导出:支持导出标注后的图片
2720
- * - TypeScript 支持:完整的类型定义
2721
- */
2722
- // 导出工具函数
2723
- /**
2724
- * Drawer 类 - 图片标注库的主入口
2725
- *
2726
- * @example
2727
- * ```typescript
2728
- * const drawer = new Drawer({
2729
- * id: 'container',
2730
- * drawType: 'rect'
2731
- * });
2732
- * drawer.drawImage('image.jpg');
2733
- * ```
2734
- */
2735
- class Drawer {
2736
- constructor(options) {
2737
- // Canvas 相关
2738
- this.canvas = document.createElement("canvas");
2739
- // 图片相关
2740
- this.bgImage = null;
2741
- this.bgImageSrc = "";
2742
- this.bgImageExt = "jpeg";
2743
- // 绘制类型
2744
- this.drawType = "";
2745
- const { drawType, useEvents = true, id = "container", annotationColor, lineStyle, vertexStyle, textStyle } = options;
2746
- // 获取容器
2747
- const container = document.getElementById(id);
2748
- if (!container)
2749
- throw new Error(`Container with id "${id}" not found`);
2750
- this.container = container;
2751
- // 确保容器有相对定位
2752
- if (container.style.position !== "absolute" &&
2753
- container.style.position !== "fixed" &&
2754
- container.style.position !== "relative") {
2755
- container.style.position = "relative";
2756
- }
2757
- // 初始化 Canvas
2758
- this.ctx = this.canvas.getContext("2d");
2759
- // 初始化视口
2760
- this.viewport = new ViewportManager();
2761
- this.viewport.setSize(container.clientWidth, container.clientHeight);
2762
- // 设置 Canvas 尺寸
2763
- this.canvas.width = this.viewport.width;
2764
- this.canvas.height = this.viewport.height;
2765
- this.canvas.style.width = `${this.viewport.width}px`;
2766
- this.canvas.style.height = `${this.viewport.height}px`;
2767
- this.canvas.style.cursor = "default";
2768
- this.canvas.style.display = "block";
2769
- container.appendChild(this.canvas);
2770
- // 初始化功能模块
2771
- this.annotationManager = new AnnotationManager(this.viewport);
2772
- // 设置颜色配置
2773
- if (annotationColor) {
2774
- this.annotationManager.setColorConfig(annotationColor);
2775
- }
2776
- // 设置边线样式
2777
- if (lineStyle) {
2778
- this.annotationManager.setLineStyle(lineStyle);
2779
- }
2780
- // 设置顶点样式
2781
- if (vertexStyle) {
2782
- this.annotationManager.setVertexStyle(vertexStyle);
2783
- }
2784
- this.textManager = new TextAnnotationManager(this.viewport, this.container, this.ctx, () => this.render());
2785
- // 设置文本样式
2786
- if (textStyle) {
2787
- this.textManager.setTextStyle(textStyle);
2788
- }
2789
- this.renderer = new Renderer(this.ctx, this.viewport, this.annotationManager, this.textManager, this.canvas);
2790
- this.eventHandler = new EventHandler(this.canvas, this.viewport, this.annotationManager, this.textManager, this.renderer, () => this.drawType, () => this.bgImage, () => this.render(), (type) => this.setDrawType(type));
2791
- // 初始化事件处理器
2792
- this.events = {
2793
- wheel: (e) => this.eventHandler.handleWheel(e),
2794
- mousedown: (e) => this.eventHandler.handleMouseDown(e),
2795
- mousemove: (e) => this.eventHandler.handleMouseMove(e),
2796
- mouseup: (e) => this.eventHandler.handleMouseUp(e),
2797
- mouseleave: (e) => this.eventHandler.handleMouseLeave(e),
2798
- dblclick: (e) => this.eventHandler.handleDoubleClick(e),
2799
- click: (e) => this.eventHandler.handleClick(e),
2800
- keydown: (e) => this.eventHandler.handleKeyDown(e),
2801
- };
2802
- // 设置绘制类型
2803
- this.setDrawType(drawType || "");
2804
- // 绑定事件
2805
- if (useEvents) {
2806
- this.addEventListeners();
2807
- }
2808
- }
2809
- /**
2810
- * 设置绘制模式
2811
- * @param type - 绘制类型: 'rect' | 'polygon' | 'drag' | 'text' | ''
2812
- */
2813
- setDrawType(type) {
2814
- // 检查当前是否正在绘制多边形
2815
- if (this.drawType === "polygon" && this.annotationManager.isDrawing) {
2816
- if (this.annotationManager.operate.data.length > 0) {
2817
- console.warn("Cannot switch mode while polygon is being drawn. Please complete or cancel the polygon first.");
2818
- return;
2819
- }
2820
- }
2821
- // 如果切换到相同模式,不做任何操作
2822
- if (this.drawType === type)
2823
- return;
2824
- // 重置当前操作状态
2825
- this.annotationManager.operate.data = [];
2826
- this.annotationManager.isDrawing = false;
2827
- this.annotationManager.tempPolygonPoint = null;
2828
- // 如果切换到绘制模式,取消选中状态
2829
- if (type !== "") {
2830
- this.deselectAnnotation();
2831
- }
2832
- // 更新操作类型和绘制类型
2833
- this.annotationManager.operate.type = type;
2834
- this.drawType = type;
2835
- // 设置光标样式
2836
- if (type === "drag") {
2837
- this.canvas.style.cursor = "grab";
2838
- }
2839
- else if (type === "text") {
2840
- this.canvas.style.cursor = "text";
2841
- }
2842
- else {
2843
- this.canvas.style.cursor = "default";
2844
- }
2845
- }
2846
- /**
2847
- * 加载背景图片
2848
- * @param src - 图片 URL
2849
- */
2850
- drawImage(src) {
2851
- this.bgImageSrc = src;
2852
- this.bgImageExt = getImageTypeFromUrl(src);
2853
- const img = new Image();
2854
- img.crossOrigin = "anonymous";
2855
- img.src = src;
2856
- img.onload = () => {
2857
- this.bgImage = img;
2858
- this.viewport.setOriginalSize(img.width, img.height);
2859
- this.viewport.calculateInitialView();
2860
- this.render();
2861
- };
2862
- }
2863
- /**
2864
- * 渲染画布
2865
- */
2866
- render() {
2867
- this.renderer.render(this.bgImage);
2868
- }
2869
- /**
2870
- * 清除所有标注
2871
- * @param type - 清除后设置的绘制类型
2872
- */
2873
- clear(type = "") {
2874
- this.annotationManager.clear();
2875
- this.textManager.clearTextAnnotations();
2876
- this.setDrawType(type);
2877
- this.render();
2878
- }
2879
- /**
2880
- * 撤销上一步操作
2881
- */
2882
- withdraw() {
2883
- const annotationWithdrawn = this.annotationManager.withdraw();
2884
- const textWithdrawn = this.textManager.withdraw();
2885
- if (annotationWithdrawn || textWithdrawn) {
2886
- this.render();
2887
- }
2888
- }
2889
- /**
2890
- * 绑定事件监听器
2891
- */
2892
- addEventListeners() {
2893
- this.canvas.addEventListener("mousedown", this.events.mousedown);
2894
- this.canvas.addEventListener("mousemove", this.events.mousemove);
2895
- this.canvas.addEventListener("mouseup", this.events.mouseup);
2896
- this.canvas.addEventListener("mouseleave", this.events.mouseleave);
2897
- this.canvas.addEventListener("wheel", this.events.wheel);
2898
- this.canvas.addEventListener("contextmenu", (e) => e.preventDefault());
2899
- document.addEventListener("keydown", this.events.keydown);
2900
- this.handleClickEventListener();
2901
- }
2902
- /**
2903
- * 处理单击事件(用于区分单击和双击)
2904
- */
2905
- handleClickEventListener() {
2906
- let clickTimeout = null;
2907
- let clickCount = 0;
2908
- this.canvas.addEventListener("click", (e) => {
2909
- // 跳过拖拽/移动/调整大小/文本移动结束后的点击
2910
- if (this.eventHandler.isDragging ||
2911
- this.annotationManager.isMovingAnnotation ||
2912
- this.annotationManager.isResizing ||
2913
- this.textManager.isTextMoving) {
2914
- return;
2915
- }
2916
- // 检查是否需要跳过这次点击
2917
- const shouldSkip = this.eventHandler.justFinishedMove;
2918
- if (shouldSkip) {
2919
- this.eventHandler.justFinishedMove = false;
2920
- }
2921
- clickCount++;
2922
- if (clickCount === 1) {
2923
- clickTimeout = window.setTimeout(() => {
2924
- if (clickCount === 1 && !shouldSkip) {
2925
- this.events.click(e);
2926
- }
2927
- clickCount = 0;
2928
- clickTimeout = null;
2929
- }, 300);
2930
- }
2931
- else if (clickCount === 2) {
2932
- this.eventHandler.justFinishedMove = false;
2933
- this.events.dblclick(e);
2934
- clickCount = 0;
2935
- if (clickTimeout !== null) {
2936
- clearTimeout(clickTimeout);
2937
- clickTimeout = null;
2938
- }
2939
- }
2940
- });
2941
- }
2942
- /**
2943
- * 改变缩放比例
2944
- * @param zoomIn - true: 放大, false: 缩小
2945
- */
2946
- changeScale(zoomIn) {
2947
- if (!this.bgImage)
2948
- return;
2949
- const delta = getZoomDelta(this.viewport.scale, zoomIn);
2950
- const centerX = this.viewport.width / 2;
2951
- const centerY = this.viewport.height / 2;
2952
- const newScale = this.viewport.scale + delta;
2953
- this.viewport.updateScale(newScale, centerX, centerY);
2954
- this.render();
2955
- }
2956
- /**
2957
- * 清除整个画布,包括背景图片和所有标注
2958
- * @param keepImage - 是否保留背景图片,默认为 false
2959
- * @param clearImage - 是否清除背景图片,默认为 true(与 keepImage 互斥)
2960
- */
2961
- clearCanvas(keepImage = false, clearImage = true) {
2962
- // 清除所有标注
2963
- this.annotationManager.clear();
2964
- this.textManager.clearTextAnnotations();
2965
- // 取消文本标注选中
2966
- this.textManager.deselectTextAnnotation();
2967
- // 只有在不清除图片且需要重置视图时才重置
2968
- if (!keepImage && clearImage) {
2969
- // 清除背景图片
2970
- this.bgImage = null;
2971
- this.bgImageSrc = "";
2972
- this.viewport.setOriginalSize(0, 0);
2973
- // 重置视图状态
2974
- this.viewport.reset();
2975
- }
2976
- // 清除画布内容
2977
- this.ctx.clearRect(0, 0, this.viewport.width, this.viewport.height);
2978
- // 如果保留图片,重新渲染
2979
- if (keepImage && this.bgImage) {
2980
- this.render();
2981
- }
2982
- // 更新光标
2983
- if (this.drawType === "drag") {
2984
- this.canvas.style.cursor = "default";
2985
- }
2986
- }
2987
- /**
2988
- * 清除所有标注但保留背景图片
2989
- */
2990
- clearAnnotations() {
2991
- this.clearCanvas(true, false);
2992
- }
2993
- /**
2994
- * 获取当前所有标注数据
2995
- */
2996
- getAnnotations() {
2997
- return this.annotationManager.getAnnotations();
2998
- }
2999
- /**
3000
- * 获取所有文本标注
3001
- */
3002
- getTextAnnotations() {
3003
- return this.textManager.getTextAnnotations();
3004
- }
3005
- /**
3006
- * 导出原始尺寸标注图片
3007
- */
3008
- exportAnnotationImage() {
3009
- return new Promise((resolve, reject) => {
3010
- if (!this.bgImage) {
3011
- reject(new Error("No background image loaded"));
3012
- return;
3013
- }
3014
- // 创建离屏 Canvas
3015
- const offscreenCanvas = document.createElement("canvas");
3016
- offscreenCanvas.width = this.viewport.originalWidth;
3017
- offscreenCanvas.height = this.viewport.originalHeight;
3018
- const offscreenCtx = offscreenCanvas.getContext("2d");
3019
- if (!offscreenCtx) {
3020
- reject(new Error("Could not create offscreen canvas context"));
3021
- return;
3022
- }
3023
- const image = new Image();
3024
- image.src = this.bgImageSrc;
3025
- image.crossOrigin = "anonymous";
3026
- image.onload = () => {
3027
- try {
3028
- this.renderer.drawForExport(offscreenCtx, image, this.viewport.originalWidth, this.viewport.originalHeight);
3029
- const base64 = offscreenCanvas.toDataURL(`image/${this.bgImageExt}`, 0.7);
3030
- resolve(base64);
3031
- }
3032
- catch (e) {
3033
- this.exportCurrentViewImage()
3034
- .then(resolve)
3035
- .catch((err) => reject(new Error(`Both methods failed: ${err.message}`)));
3036
- }
3037
- };
3038
- image.onerror = () => {
3039
- reject(new Error("Failed to load image for export"));
3040
- };
3041
- });
3042
- }
3043
- /**
3044
- * 导出当前视图为 base64 图片
3045
- */
3046
- exportCurrentViewImage() {
3047
- return new Promise((resolve, reject) => {
3048
- try {
3049
- const base64 = this.canvas.toDataURL(`image/${this.bgImageExt}`);
3050
- resolve(base64);
3051
- }
3052
- catch (e) {
3053
- reject(new Error(`Failed to export current view: ${e.message}`));
3054
- }
3055
- });
3056
- }
3057
- /**
3058
- * 将 base64 转换为 File 对象
3059
- */
3060
- base64ToFile(base64Data, filename) {
3061
- return base64ToFile(base64Data, filename);
3062
- }
3063
- /**
3064
- * 将 base64 转换为 Blob 对象(静态方法)
3065
- */
3066
- static base64ToBlob(base64) {
3067
- return base64ToBlob(base64);
3068
- }
3069
- // ==================== 文本标注 API ====================
3070
- /**
3071
- * 添加文本标注
3072
- * @param x - 图像坐标 X
3073
- * @param y - 图像坐标 Y
3074
- * @param text - 初始文本(可选,默认为空字符串)
3075
- */
3076
- addTextAnnotation(x, y, text = "") {
3077
- this.textManager.addTextAnnotation(x, y, text);
3078
- this.render();
3079
- }
3080
- /**
3081
- * 更新文本标注内容
3082
- * @param index - 文本标注索引
3083
- * @param text - 新文本内容
3084
- */
3085
- updateTextAnnotation(index, text) {
3086
- if (this.textManager.updateTextAnnotation(index, text)) {
3087
- this.render();
3088
- }
3089
- }
3090
- /**
3091
- * 移动文本标注位置
3092
- * @param index - 文本标注索引
3093
- * @param x - 新位置 X(图像坐标)
3094
- * @param y - 新位置 Y(图像坐标)
3095
- */
3096
- moveTextAnnotation(index, x, y) {
3097
- if (this.textManager.moveTextAnnotation(index, x, y)) {
3098
- this.render();
3099
- }
3100
- }
3101
- /**
3102
- * 删除文本标注
3103
- * @param index - 文本标注索引
3104
- */
3105
- removeTextAnnotation(index) {
3106
- if (this.textManager.removeTextAnnotation(index)) {
3107
- this.render();
3108
- }
3109
- }
3110
- /**
3111
- * 清除所有文本标注
3112
- */
3113
- clearTextAnnotations() {
3114
- this.textManager.clearTextAnnotations();
3115
- this.render();
3116
- }
3117
- // ==================== 标注选中与移动 API ====================
3118
- /**
3119
- * 选中指定索引的标注
3120
- * @param index - 标注索引
3121
- */
3122
- selectAnnotation(index) {
3123
- if (this.annotationManager.selectAnnotation(index)) {
3124
- this.render();
3125
- }
3126
- }
3127
- /**
3128
- * 取消选中
3129
- */
3130
- deselectAnnotation() {
3131
- this.annotationManager.deselectAnnotation();
3132
- this.textManager.deselectTextAnnotation();
3133
- this.textManager.editingTextIndex = null;
3134
- this.render();
3135
- }
3136
- /**
3137
- * 获取当前选中的标注信息
3138
- */
3139
- getSelectedAnnotation() {
3140
- // 优先返回矩形/多边形的选中
3141
- const shapeSelected = this.annotationManager.getSelectedAnnotation();
3142
- if (shapeSelected)
3143
- return shapeSelected;
3144
- // 返回文本标注的选中
3145
- const textSelected = this.textManager.getSelectedTextAnnotation();
3146
- if (textSelected) {
3147
- return {
3148
- index: textSelected.index,
3149
- type: "text",
3150
- data: textSelected.data
3151
- };
3152
- }
3153
- return null;
3154
- }
3155
- /**
3156
- * 删除选中的标注
3157
- */
3158
- deleteSelectedAnnotation() {
3159
- // 优先删除选中的文本标注
3160
- if (this.textManager.selectedTextIndex !== null) {
3161
- this.textManager.deleteSelectedTextAnnotation();
3162
- this.render();
3163
- return;
3164
- }
3165
- // 删除选中的矩形/多边形标注
3166
- if (this.annotationManager.deleteSelectedAnnotation()) {
3167
- this.render();
3168
- }
3169
- }
3170
- /**
3171
- * 移动选中的标注
3172
- * @param dx - X 方向移动距离
3173
- * @param dy - Y 方向移动距离
3174
- */
3175
- moveSelectedAnnotation(dx, dy) {
3176
- if (this.annotationManager.moveSelectedAnnotation(dx, dy)) {
3177
- this.render();
3178
- }
3179
- }
3180
- // ==================== 颜色配置 API ====================
3181
- /**
3182
- * 设置标注颜色配置
3183
- * 新设置的样式只影响之后创建的标注
3184
- * 如果当前有选中的标注,会实时预览新样式(但不会保存,除非调用 updateSelectedAnnotationStyle)
3185
- * @param color - 颜色字符串或颜色配置对象
3186
- * @example
3187
- * // 设置所有标注为同一种颜色
3188
- * drawer.setAnnotationColor('#FF0000')
3189
- *
3190
- * // 为不同类型设置不同颜色
3191
- * drawer.setAnnotationColor({
3192
- * rect: '#FF0000',
3193
- * polygon: '#00FF00',
3194
- * default: '#0000FF'
3195
- * })
3196
- */
3197
- setAnnotationColor(color) {
3198
- this.annotationManager.setColorConfig(color);
3199
- this.render();
3200
- }
3201
- /**
3202
- * 获取当前颜色配置
3203
- */
3204
- getAnnotationColor() {
3205
- return this.annotationManager.getColorConfig();
3206
- }
3207
- /**
3208
- * 更新选中标注的样式为当前设置的样式
3209
- * 调用此方法后,选中的标注会保存当前的新样式
3210
- */
3211
- updateSelectedAnnotationStyle() {
3212
- const selected = this.annotationManager.selectedAnnotation;
3213
- if (!selected)
3214
- return false;
3215
- const annotation = this.annotationManager.recordList[selected.index];
3216
- if (!annotation)
3217
- return false;
3218
- // 更新标注的样式为当前样式
3219
- annotation.style = this.annotationManager.getCurrentStyle();
3220
- this.render();
3221
- return true;
3222
- }
3223
- // ==================== 边线和顶点样式 API ====================
3224
- /**
3225
- * 设置边线样式
3226
- * @param style - 边线样式:'solid' | 'dashed' | 'dotted'
3227
- */
3228
- setLineStyle(style) {
3229
- this.annotationManager.setLineStyle(style);
3230
- this.render();
3231
- }
3232
- /**
3233
- * 获取当前边线样式
3234
- */
3235
- getLineStyle() {
3236
- return this.annotationManager.getLineStyle();
3237
- }
3238
- /**
3239
- * 设置顶点样式
3240
- * @param style - 顶点样式配置
3241
- * @example
3242
- * drawer.setVertexStyle({
3243
- * size: 10,
3244
- * fillColor: '#FF0000',
3245
- * strokeColor: '#FFFFFF',
3246
- * strokeWidth: 2,
3247
- * shape: 'circle' // 'circle' | 'square' | 'diamond'
3248
- * })
3249
- */
3250
- setVertexStyle(style) {
3251
- this.annotationManager.setVertexStyle(style);
3252
- this.render();
3253
- }
3254
- /**
3255
- * 获取当前顶点样式
3256
- */
3257
- getVertexStyle() {
3258
- return this.annotationManager.getVertexStyle();
3259
- }
3260
- // ==================== 文本样式 API ====================
3261
- /**
3262
- * 设置文本标注样式
3263
- * @param style - 文本样式配置
3264
- * @example
3265
- * drawer.setTextStyle({
3266
- * font: '16px Arial',
3267
- * color: '#FFD700',
3268
- * backgroundColor: 'rgba(0,0,0,0.6)'
3269
- * })
3270
- */
3271
- setTextStyle(style) {
3272
- this.textManager.setTextStyle(style);
3273
- this.render();
3274
- }
3275
- /**
3276
- * 设置文本输入框样式
3277
- * @param style - 输入框样式配置
3278
- * @example
3279
- * drawer.setTextInputStyle({
3280
- * border: '2px solid #00D9FF',
3281
- * borderRadius: '6px',
3282
- * backgroundColor: '#ffffff',
3283
- * color: '#333'
3284
- * })
3285
- */
3286
- setTextInputStyle(style) {
3287
- this.textManager.setInputStyle(style);
3288
- }
3289
- /**
3290
- * 设置文本标注选中态样式
3291
- * @param style - 选中态样式配置
3292
- * @example
3293
- * drawer.setTextSelectionStyle({
3294
- * selectedBorderColor: '#00D9FF',
3295
- * selectedBackgroundColor: 'rgba(0,217,255,0.15)'
3296
- * })
3297
- */
3298
- setTextSelectionStyle(style) {
3299
- this.textManager.setSelectionStyle(style);
3300
- this.render();
3301
- }
3302
- /**
3303
- * 销毁 Drawer 实例,清理资源
3304
- */
3305
- destroy() {
3306
- // 清理文本输入框
3307
- this.textManager.destroy();
3308
- // 移除 Canvas
3309
- if (this.canvas.parentNode) {
3310
- this.canvas.parentNode.removeChild(this.canvas);
3311
- }
3312
- }
3313
- }
3314
-
3315
- exports.AnnotationManager = AnnotationManager;
3316
- exports.Drawer = Drawer;
3317
- exports.EventHandler = EventHandler;
3318
- exports.Renderer = Renderer;
3319
- exports.TextAnnotationManager = TextAnnotationManager;
3320
- exports.ViewportManager = ViewportManager;
3321
- exports.base64ToBlob = base64ToBlob;
3322
- exports.base64ToFile = base64ToFile;
3323
- exports.default = Drawer;
3324
- exports.getImageTypeFromUrl = getImageTypeFromUrl;
3325
- exports.getZoomDelta = getZoomDelta;
3326
- exports.isPointInPolygon = isPointInPolygon;
3327
- exports.isPointInRect = isPointInRect;
3328
-
3329
- Object.defineProperty(exports, '__esModule', { value: true });
3330
-
3331
- }));
3332
- if (typeof ImageAnnotationDrawer !== "undefined" && ImageAnnotationDrawer.default) { ImageAnnotationDrawer = ImageAnnotationDrawer.default; }
3333
- //# sourceMappingURL=index.js.map
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).ImageAnnotationDrawer=t.ImageAnnotationDrawer||{})}(this,function(t){"use strict";function e(t,e){const n=e.width>=0?e.start.x:e.start.x+e.width,i=e.height>=0?e.start.y:e.start.y+e.height,s=Math.abs(e.width),a=Math.abs(e.height);return t.x>=n&&t.x<=n+s&&t.y>=i&&t.y<=i+a}function n(t,e){if(e.length<3)return!1;let n=!1;for(let i=0,s=e.length-1;i<e.length;s=i++){const a=e[i].point.x,o=e[i].point.y,r=e[s].point.x,h=e[s].point.y;o>t.y!=h>t.y&&t.x<(r-a)*(t.y-o)/(h-o)+a&&(n=!n)}return n}function i(t){try{const e=new URL(t).pathname.toLowerCase();return e.endsWith(".png")?"png":e.endsWith(".jpg")?"jpg":e.endsWith(".jpeg")?"jpeg":e.endsWith(".webp")?"webp":e.endsWith(".gif")?"gif":e.endsWith(".bmp")?"bmp":e.endsWith(".svg")?"svg":"jpeg"}catch(t){return"jpeg"}}function s(t,e){const n=t.split(";base64,"),i=n[0].split(":")[1],s=window.atob(n[1]),a=s.length,o=new Uint8Array(a);for(let t=0;t<a;++t)o[t]=s.charCodeAt(t);const r=new Blob([o],{type:i});return new File([r],e,{type:i})}function a(t){return new Promise((e,n)=>{const i=t.split(";base64,");if(i.length<2)return void n(new Error("Invalid base64 string"));const s=i[0].split(":")[1],a=atob(i[1]),o=new ArrayBuffer(a.length),r=new Uint8Array(o);for(let t=0;t<a.length;t++)r[t]=a.charCodeAt(t);e(new Blob([o],{type:s}))})}function o(t,e){return t<2?e?.1:-.1:t<5?e?.05:-.05:e?.02:-.02}class r{constructor(){this.width=0,this.height=0,this.originalWidth=0,this.originalHeight=0,this.scale=1,this.minScale=1,this.maxScale=10,this.offset={x:0,y:0},this.initialScale=1,this.initialOffset={x:0,y:0},this.isInitialScale=!0,this.minOffsetX=0,this.minOffsetY=0,this.maxOffsetX=0,this.maxOffsetY=0}setSize(t,e){this.width=t,this.height=e}setOriginalSize(t,e){this.originalWidth=t,this.originalHeight=e}calculateInitialView(){if(0===this.originalWidth||0===this.originalHeight)return;const t=this.width/this.originalWidth,e=this.height/this.originalHeight;this.scale=Math.min(t,e),this.initialScale=this.scale,this.minScale=this.initialScale,this.offset={x:(this.width-this.originalWidth*this.scale)/2,y:(this.height-this.originalHeight*this.scale)/2},this.initialOffset=Object.assign({},this.offset),this.calculateBoundaries(),this.isInitialScale=!0}calculateBoundaries(){const t=this.originalWidth*this.scale,e=this.originalHeight*this.scale;this.maxOffsetX=0,this.maxOffsetY=0,this.minOffsetX=this.width-t,this.minOffsetY=this.height-e,this.minOffsetX>this.maxOffsetX&&([this.minOffsetX,this.maxOffsetX]=[this.maxOffsetX,this.minOffsetX]),this.minOffsetY>this.maxOffsetY&&([this.minOffsetY,this.maxOffsetY]=[this.maxOffsetY,this.minOffsetY])}constrainViewport(){this.offset.x>this.maxOffsetX?this.offset.x=this.maxOffsetX:this.offset.x<this.minOffsetX&&(this.offset.x=this.minOffsetX),this.offset.y>this.maxOffsetY?this.offset.y=this.maxOffsetY:this.offset.y<this.minOffsetY&&(this.offset.y=this.minOffsetY)}toImageCoordinates(t,e){return{x:(t-this.offset.x)/this.scale,y:(e-this.offset.y)/this.scale}}toCanvasCoordinates(t,e){return{x:this.offset.x+t*this.scale,y:this.offset.y+e*this.scale}}updateScale(t,e,n){const i=this.scale,s=(e-this.offset.x)/i,a=(n-this.offset.y)/i;this.scale=Math.max(this.minScale,Math.min(t,this.maxScale));const o=s*this.scale+this.offset.x,r=a*this.scale+this.offset.y;this.offset.x+=e-o,this.offset.y+=n-r,this.calculateBoundaries(),this.constrainViewport(),this.isInitialScale=this.scale===this.initialScale}resetToInitial(){this.scale=this.initialScale,this.offset=Object.assign({},this.initialOffset),this.calculateBoundaries(),this.isInitialScale=!0}updateOffset(t,e){this.offset.x+=t,this.offset.y+=e,this.constrainViewport()}reset(){this.scale=1,this.minScale=1,this.offset={x:0,y:0},this.initialScale=1,this.initialOffset={x:0,y:0},this.isInitialScale=!0,this.minOffsetX=0,this.minOffsetY=0,this.maxOffsetX=0,this.maxOffsetY=0}}class h{constructor(t){this.viewport=t,this.recordList=[],this.operate={type:"rect",data:[],status:"pending"},this.isDrawing=!1,this.drawStartPoint={x:0,y:0},this.tempPolygonPoint=null,this.selectedAnnotation=null,this.isMovingAnnotation=!1,this.isResizing=!1,this.activeHandle=null,this.annotationMoveStart={x:0,y:0},this.originalRect=null,this.originalPolygon=null,this.deleteHistory=[],this.strokeStyle="red",this.lineWidth=5,this.lineStyle="solid",this.vertexStyle={size:8,fillColor:"red",strokeColor:"white",strokeWidth:2,shape:"circle"},this.selectionStyle={strokeColor:"#00D9FF",fillColor:"rgba(0,217,255,0.15)",handleSize:12,handleColor:"#00D9FF"},this.colorConfig={rect:"red",polygon:"red",default:"red"},this.applyColorConfig()}setColorConfig(t){this.colorConfig="string"==typeof t?{rect:t,polygon:t,default:t}:Object.assign(Object.assign({},this.colorConfig),t),this.applyColorConfig()}getColorConfig(){return Object.assign({},this.colorConfig)}applyColorConfig(){this.strokeStyle=this.colorConfig.default||"red"}getColorForType(t){return this.colorConfig[t]||this.colorConfig.default||"red"}getCurrentStyle(){return{strokeColor:this.strokeStyle,lineWidth:this.lineWidth,lineStyle:this.lineStyle,vertexStyle:Object.assign({},this.vertexStyle)}}setLineStyle(t){this.lineStyle=t}setVertexStyle(t){this.vertexStyle=Object.assign(Object.assign({},this.vertexStyle),t),t.fillColor&&(this.strokeStyle=t.fillColor)}getLineStyle(){return this.lineStyle}getVertexStyle(){return Object.assign({},this.vertexStyle)}getAnnotationStyle(t){return t.style||this.getCurrentStyle()}startRectDrawing(t){this.isDrawing=!0,this.drawStartPoint=t,this.operate={type:"rect",data:[{start:Object.assign({},t),width:0,height:0}],status:"pending"}}updateRectDrawing(t){if(!this.isDrawing||"rect"!==this.operate.type)return;const e=this.operate.data[0];e.width=t.x-this.drawStartPoint.x,e.height=t.y-this.drawStartPoint.y}finishRectDrawing(){if(!this.isDrawing||"rect"!==this.operate.type)return!1;const t=this.operate.data[0];if(Math.abs(t.width)>5&&Math.abs(t.height)>5){const t=Object.assign(Object.assign({},this.operate),{style:this.getCurrentStyle()});this.recordList.push(t),this.deleteHistory=[]}return this.operate.data=[],this.isDrawing=!1,!0}startPolygonDrawing(t){this.isDrawing=!0,this.operate={type:"polygon",data:[{point:t}],status:"pending"},this.tempPolygonPoint=t}addPolygonPoint(t){if(!this.isDrawing||"polygon"!==this.operate.type)return!1;const e=this.operate.data.length>0?this.operate.data[this.operate.data.length-1]:null;return!(e&&Math.abs(e.point.x-t.x)<=5&&Math.abs(e.point.y-t.y)<=5)&&(this.operate.data.push({point:t}),!0)}updatePolygonTempPoint(t){this.tempPolygonPoint=t}finishPolygonDrawing(){if(!this.isDrawing||"polygon"!==this.operate.type)return!1;if(this.operate.data.length>=3){this.operate.status="fullfilled";const t=Object.assign(Object.assign({},this.operate),{style:this.getCurrentStyle()});this.recordList.push(t),this.deleteHistory=[]}return this.operate={type:"polygon",data:[],status:"pending"},this.isDrawing=!1,this.tempPolygonPoint=null,!0}cancelDrawing(){if(this.isDrawing&&"polygon"===this.operate.type&&this.operate.data.length>=2){this.operate.status="fullfilled";const t=Object.assign(Object.assign({},this.operate),{style:this.getCurrentStyle()});this.recordList.push(t)}this.operate.data=[],this.isDrawing=!1,this.tempPolygonPoint=null}withdraw(){if(this.operate.data.length>0)return"polygon"===this.operate.type?(this.operate.data.pop(),0===this.operate.data.length&&(this.isDrawing=!1)):(this.operate.data=[],this.isDrawing=!1),!0;if(this.deleteHistory.length>0){const{annotation:t,index:e}=this.deleteHistory.pop();return this.recordList.splice(e,0,t),!0}return this.recordList.length>0&&(this.recordList.pop(),!0)}selectAnnotation(t){if(t<0||t>=this.recordList.length)return this.deselectAnnotation(),!1;const e=this.recordList[t];return this.selectedAnnotation={index:t,type:e.type},!0}deselectAnnotation(){this.selectedAnnotation=null,this.isMovingAnnotation=!1,this.isResizing=!1,this.activeHandle=null,this.originalRect=null,this.originalPolygon=null}deleteSelectedAnnotation(){if(!this.selectedAnnotation)return!1;const t=this.selectedAnnotation.index,e=this.recordList[t];return this.deleteHistory.push({annotation:Object.assign({},e),index:t}),this.recordList.splice(t,1),this.deselectAnnotation(),!0}startMovingAnnotation(t){return!!this.selectedAnnotation&&(this.isMovingAnnotation=!0,this.annotationMoveStart={x:t.clientX,y:t.clientY},!0)}moveSelectedAnnotation(t,e){if(!this.selectedAnnotation)return!1;const n=this.recordList[this.selectedAnnotation.index];if(!n)return!1;if("rect"===n.type){const i=n.data[0];i.start.x+=t,i.start.y+=e}else if("polygon"===n.type){n.data.forEach(n=>{n.point.x+=t,n.point.y+=e})}return!0}finishMovingAnnotation(){this.isMovingAnnotation=!1,this.isResizing=!1,this.activeHandle=null,this.originalRect=null,this.originalPolygon=null}startResizing(t,e){if(!this.selectedAnnotation)return!1;this.activeHandle=t,this.isResizing=!0;const n=this.recordList[this.selectedAnnotation.index];return"rect"===n.type?this.originalRect=Object.assign({},n.data[0]):"polygon"===n.type&&(this.originalPolygon=n.data.map(t=>({point:Object.assign({},t.point)}))),!0}resizeRect(t){if(!this.selectedAnnotation||"rect"!==this.selectedAnnotation.type||!this.originalRect||!this.activeHandle)return!1;const e=this.recordList[this.selectedAnnotation.index].data[0],n=this.originalRect,i=n.start.x,s=n.start.x+n.width,a=n.start.y,o=n.start.y+n.height;let r,h;switch(this.activeHandle.index){case 0:r=s,h=o;break;case 1:r=i,h=o;break;case 2:r=s,h=a;break;case 3:r=i,h=a;break;default:return!1}const l=Math.min(t.x,r),c=Math.max(t.x,r),d=Math.min(t.y,h),g=Math.max(t.y,h);e.start.x=l,e.start.y=d,e.width=c-l,e.height=g-d;const x=t.x<r,f=t.y<h;return 0===this.activeHandle.index?x||f?!x&&f?this.activeHandle.index=1:x&&!f&&(this.activeHandle.index=2):this.activeHandle.index=3:1===this.activeHandle.index?x&&!f?this.activeHandle.index=2:x&&f?this.activeHandle.index=0:x||f||(this.activeHandle.index=3):2===this.activeHandle.index?!x&&f?this.activeHandle.index=1:x&&f?this.activeHandle.index=0:x||f||(this.activeHandle.index=3):3===this.activeHandle.index&&(x&&f?this.activeHandle.index=0:!x&&f?this.activeHandle.index=1:x&&!f&&(this.activeHandle.index=2)),this.originalRect=Object.assign({},e),!0}resizePolygon(t){if(!this.selectedAnnotation||"polygon"!==this.selectedAnnotation.type||!this.activeHandle)return!1;const e=this.recordList[this.selectedAnnotation.index].data,n=this.activeHandle.index;return n>=0&&n<e.length&&(e[n].point.x=t.x,e[n].point.y=t.y,!0)}getAnnotationAtPoint(t){for(let i=this.recordList.length-1;i>=0;i--){const s=this.recordList[i];if("rect"===s.type){if(e(t,s.data[0]))return{index:i,type:"rect"}}else if("polygon"===s.type){if(n(t,s.data))return{index:i,type:"polygon"}}}return null}getHandleAtPoint(t,e){if(!this.selectedAnnotation)return null;const n=this.recordList[this.selectedAnnotation.index],i=this.selectionStyle.handleSize/2;if("rect"===n.type){const s=n.data[0],a=this.viewport.offset.x+s.start.x*this.viewport.scale,o=this.viewport.offset.y+s.start.y*this.viewport.scale,r=s.width*this.viewport.scale,h=s.height*this.viewport.scale,l=[{x:a,y:o,index:0},{x:a+r,y:o,index:1},{x:a,y:o+h,index:2},{x:a+r,y:o+h,index:3}];for(const n of l)if(t>=n.x-i&&t<=n.x+i&&e>=n.y-i&&e<=n.y+i)return{type:"rect-corner",index:n.index}}else if("polygon"===n.type){const s=n.data;for(let n=0;n<s.length;n++){const a=s[n],o=this.viewport.offset.x+a.point.x*this.viewport.scale,r=this.viewport.offset.y+a.point.y*this.viewport.scale;if(t>=o-i&&t<=o+i&&e>=r-i&&e<=r+i)return{type:"polygon-vertex",index:n}}}return null}clear(){this.recordList=[],this.operate={type:"rect",data:[],status:"pending"},this.isDrawing=!1,this.tempPolygonPoint=null,this.deselectAnnotation(),this.deleteHistory=[]}getAnnotations(){return[...this.recordList]}getSelectedAnnotation(){if(!this.selectedAnnotation)return null;const t=this.recordList[this.selectedAnnotation.index];return t?{index:this.selectedAnnotation.index,type:this.selectedAnnotation.type,data:t}:null}}class l{constructor(t,e,n,i){this.viewport=t,this.container=e,this.ctx=n,this.renderCallback=i,this.textAnnotations=[],this.editingTextIndex=null,this.currentEditingIndex=null,this.isTextMoving=!1,this.textMoveStart={x:0,y:0},this.textBeforeEditing="",this.selectedTextIndex=null,this.textInput=null,this.deleteHistory=[],this.defaultInputStyle={border:"2px solid #00D9FF",borderRadius:"6px",padding:"6px 10px",fontSize:"16px",fontFamily:"Arial, sans-serif",backgroundColor:"#ffffff",color:"#333",boxShadow:"0 4px 12px rgba(0,0,0,0.3)",minWidth:"60px",maxWidth:"200px"},this.textStyle={font:"16px Arial",color:"#FFD700",padding:6,backgroundColor:"rgba(0,0,0,0.6)",borderRadius:4,selectedBorderColor:"#00D9FF",selectedBackgroundColor:"rgba(0,217,255,0.15)",inputStyle:Object.assign({},this.defaultInputStyle)},this.createTextInput()}createTextInput(){this.textInput=document.createElement("input"),this.textInput.type="text",this.textInput.style.position="absolute",this.textInput.style.zIndex="99999",this.textInput.style.display="none",this.textInput.style.outline="none",this.textInput.style.width="fit-content",this.textInput.placeholder="输入文字...",this.applyInputStyle(),this.textInput.addEventListener("blur",()=>{this.finishEditing(),this.renderCallback&&this.renderCallback()}),this.textInput.addEventListener("keydown",t=>{t.stopPropagation(),"Enter"===t.key?(this.finishEditing(),this.renderCallback&&this.renderCallback()):"Escape"===t.key?(t.preventDefault(),this.cancelEditing()):"Delete"===t.key&&""===this.textInput.value&&(t.preventDefault(),this.deleteEditingAnnotation())}),this.textInput.addEventListener("input",()=>{this.adjustInputWidth()}),this.container.appendChild(this.textInput)}applyInputStyle(){if(!this.textInput)return;const t=Object.assign(Object.assign({},this.defaultInputStyle),this.textStyle.inputStyle);this.textInput.style.border=t.border,this.textInput.style.borderRadius=t.borderRadius,this.textInput.style.padding=t.padding,this.textInput.style.fontSize=t.fontSize,this.textInput.style.fontFamily=t.fontFamily,this.textInput.style.backgroundColor=t.backgroundColor,this.textInput.style.color=t.color,this.textInput.style.boxShadow=t.boxShadow,this.textInput.style.minWidth=t.minWidth,this.textInput.style.maxWidth=t.maxWidth}setTextStyle(t){this.textStyle=Object.assign(Object.assign({},this.textStyle),t),this.applyInputStyle()}setInputStyle(t){this.textStyle.inputStyle=Object.assign(Object.assign({},this.textStyle.inputStyle),t),this.applyInputStyle()}setSelectionStyle(t){void 0!==t.selectedBorderColor&&(this.textStyle.selectedBorderColor=t.selectedBorderColor),void 0!==t.selectedBackgroundColor&&(this.textStyle.selectedBackgroundColor=t.selectedBackgroundColor)}getCurrentStyle(){return{font:this.textStyle.font,color:this.textStyle.color,backgroundColor:this.textStyle.backgroundColor}}addTextAnnotation(t,e,n=""){var i;this.ctx.font=this.textStyle.font;const s=n||"输入文字...",a=this.ctx.measureText(s);let o;if(a.actualBoundingBoxAscent&&a.actualBoundingBoxDescent)o=a.actualBoundingBoxAscent+a.actualBoundingBoxDescent;else{o=1.2*parseInt((null===(i=this.textStyle.font.match(/\d+/))||void 0===i?void 0:i[0])||"16")}const r={position:{x:t,y:e},text:n,style:this.getCurrentStyle(),width:Math.max(a.width,60),height:o};this.textAnnotations.push(r);const h=this.textAnnotations.length-1;return this.deleteHistory=[],this.startEditing(h),h}updateTextAnnotation(t,e){if(t<0||t>=this.textAnnotations.length)return!1;const n=this.textAnnotations[t];n.text=e,this.ctx.font=this.textStyle.font;const i=this.ctx.measureText(e);return n.width=i.width,n.height=1.2*parseInt(this.textStyle.font),!0}moveTextAnnotation(t,e,n){return!(t<0||t>=this.textAnnotations.length)&&(this.textAnnotations[t].position={x:e,y:n},!0)}removeTextAnnotation(t){if(t<0||t>=this.textAnnotations.length)return!1;const e=this.textAnnotations[t];return this.deleteHistory.push({annotation:Object.assign({},e),index:t}),this.textAnnotations.splice(t,1),this.editingTextIndex===t?this.cancelEditing():null!==this.editingTextIndex&&this.editingTextIndex>t&&this.editingTextIndex--,!0}clearTextAnnotations(){this.textAnnotations=[],this.selectedTextIndex=null,this.cancelEditing(),this.deleteHistory=[]}startEditing(t){if(!this.textInput||t<0||t>=this.textAnnotations.length)return!1;const e=this.textAnnotations[t];this.editingTextIndex=t,this.currentEditingIndex=t,this.textBeforeEditing=e.text;const n=this.viewport.offset.x+e.position.x*this.viewport.scale,i=this.viewport.offset.y+e.position.y*this.viewport.scale;return this.textInput.value=e.text,this.textInput.style.left=n-this.textStyle.padding+"px",this.textInput.style.top=i-this.textStyle.padding+"px",this.textInput.style.display="block",this.adjustInputWidth(),setTimeout(()=>{this.textInput&&(this.textInput.focus(),this.textInput.select())},10),!0}finishEditing(){if(!this.textInput)return!1;const t=this.currentEditingIndex,e=this.textInput.value.trim();if(null===t||t<0||t>=this.textAnnotations.length)return this.resetEditingState(),!1;if(""===e)this.textAnnotations.splice(t,1);else{const n=this.textAnnotations[t];n.text=e,this.ctx.font=this.textStyle.font;const i=this.ctx.measureText(n.text);n.width=i.width,n.height=1.2*parseInt(this.textStyle.font)}return this.resetEditingState(),!0}cancelEditing(){if(!this.textInput||null===this.editingTextIndex)return!1;this.textInput.value=this.textBeforeEditing;const t=this.editingTextIndex;if(t>=0&&t<this.textAnnotations.length){""===this.textAnnotations[t].text&&""===this.textBeforeEditing&&this.textAnnotations.splice(t,1)}return this.resetEditingState(),!0}deleteEditingAnnotation(){if(null===this.editingTextIndex)return!1;const t=this.editingTextIndex;return this.textInput&&(this.textInput.style.display="none"),t>=0&&t<this.textAnnotations.length&&this.textAnnotations.splice(t,1),this.resetEditingState(),!0}resetEditingState(){this.textInput&&(this.textInput.style.display="none"),this.editingTextIndex=null,this.currentEditingIndex=null,this.textBeforeEditing=""}adjustInputWidth(){if(!this.textInput||null===this.editingTextIndex)return;this.ctx.font=this.textStyle.font;const t=this.textInput.value||this.textInput.placeholder||"",e=this.ctx.measureText(t).width,n=Math.min(Math.max(e+24,60),200);this.textInput.style.width=`${n}px`}startMoving(t,e){return!(e<0||e>=this.textAnnotations.length)&&(this.editingTextIndex=e,this.isTextMoving=!0,this.textMoveStart={x:t.clientX,y:t.clientY},!0)}moveAnnotation(t){if(!this.isTextMoving||null===this.editingTextIndex)return!1;const e=t.clientX-this.textMoveStart.x,n=t.clientY-this.textMoveStart.y,i=this.textAnnotations[this.editingTextIndex];return!!i&&(i.position.x+=e/this.viewport.scale,i.position.y+=n/this.viewport.scale,this.textMoveStart={x:t.clientX,y:t.clientY},!0)}finishMoving(){this.isTextMoving=!1}checkTextClick(t,e){for(let n=this.textAnnotations.length-1;n>=0;n--){const i=this.textAnnotations[n],s=this.viewport.offset.x+i.position.x*this.viewport.scale,a=this.viewport.offset.y+i.position.y*this.viewport.scale;if(t>=s-this.textStyle.padding&&t<=s+i.width+this.textStyle.padding&&e>=a-this.textStyle.padding&&e<=a+i.height+this.textStyle.padding)return n}return null}checkTextClickForMove(t){const e=this.checkTextClick(t.offsetX,t.offsetY);return null!==e?(this.startMoving(t,e),{index:e,handled:!0}):{index:-1,handled:!1}}getTextAnnotations(){return[...this.textAnnotations]}withdraw(){if(this.deleteHistory.length>0){const{annotation:t,index:e}=this.deleteHistory.pop();return this.textAnnotations.splice(e,0,t),!0}return this.textAnnotations.length>0&&(this.textAnnotations.pop(),!0)}selectTextAnnotation(t){return t<0||t>=this.textAnnotations.length?(this.deselectTextAnnotation(),!1):(this.selectedTextIndex=t,null!==this.editingTextIndex&&this.finishEditing(),!0)}deselectTextAnnotation(){this.selectedTextIndex=null}deleteSelectedTextAnnotation(){if(null===this.selectedTextIndex)return!1;const t=this.selectedTextIndex,e=this.textAnnotations[t];return this.deleteHistory.push({annotation:Object.assign({},e),index:t}),this.textAnnotations.splice(t,1),this.selectedTextIndex=null,null!==this.editingTextIndex&&this.editingTextIndex>t&&this.editingTextIndex--,!0}getSelectedTextAnnotation(){if(null===this.selectedTextIndex)return null;const t=this.textAnnotations[this.selectedTextIndex];return t?{index:this.selectedTextIndex,data:t}:null}destroy(){this.textInput&&this.textInput.parentNode&&this.textInput.parentNode.removeChild(this.textInput)}}class c{constructor(t,e,n,i,s){this.ctx=t,this.viewport=e,this.annotationManager=n,this.textManager=i,this.canvas=s}render(t){this.ctx.clearRect(0,0,this.viewport.width,this.viewport.height),t&&this.drawBackgroundImage(t),this.drawAnnotations(),this.drawTextAnnotations()}drawBackgroundImage(t){this.ctx.drawImage(t,this.viewport.offset.x,this.viewport.offset.y,this.viewport.originalWidth*this.viewport.scale,this.viewport.originalHeight*this.viewport.scale)}drawAnnotations(){this.annotationManager.recordList.forEach((t,e)=>{var n;const i=this.annotationManager.getAnnotationStyle(t);this.ctx.strokeStyle=i.strokeColor,this.ctx.lineWidth=i.lineWidth,this.setLineDash(i.lineStyle||"solid"),this.ctx.fillStyle=this.ctx.strokeStyle;const s=(null===(n=this.annotationManager.selectedAnnotation)||void 0===n?void 0:n.index)===e;if("rect"===t.type)this.drawRect(t.data[0],s);else if("polygon"===t.type){const e=i.vertexStyle||this.annotationManager.getVertexStyle();this.drawPolygon(t.data,"fullfilled"===t.status,e)}}),this.annotationManager.operate.data.length>0&&(this.ctx.strokeStyle=this.annotationManager.strokeStyle,this.ctx.lineWidth=this.annotationManager.lineWidth,this.ctx.fillStyle=this.ctx.strokeStyle,this.setLineDash(this.annotationManager.lineStyle),"rect"===this.annotationManager.operate.type?this.drawRect(this.annotationManager.operate.data[0],!1):"polygon"===this.annotationManager.operate.type&&this.drawCurrentPolygon()),this.ctx.setLineDash([]),this.drawSelectionHighlight()}setLineDash(t){switch(t){case"dashed":this.ctx.setLineDash([10,5]);break;case"dotted":this.ctx.setLineDash([2,4]);break;default:this.ctx.setLineDash([])}}drawRect(t,e=!1){this.ctx.strokeRect(this.viewport.offset.x+t.start.x*this.viewport.scale,this.viewport.offset.y+t.start.y*this.viewport.scale,t.width*this.viewport.scale,t.height*this.viewport.scale)}drawPolygon(t,e,n){if(0!==t.length){if(this.ctx.beginPath(),t.forEach((t,e)=>{const n=Math.round(this.viewport.offset.x+t.point.x*this.viewport.scale),i=Math.round(this.viewport.offset.y+t.point.y*this.viewport.scale);0===e?this.ctx.moveTo(n,i):this.ctx.lineTo(n,i)}),e&&t.length>1){const e=t[0];this.ctx.lineTo(this.viewport.offset.x+e.point.x*this.viewport.scale,this.viewport.offset.y+e.point.y*this.viewport.scale)}this.ctx.stroke(),this.drawPolygonVertices(t,n)}}drawPolygonVertices(t,e){const n=e||this.annotationManager.getVertexStyle(),i=n.size/2;t.forEach(t=>{const e=Math.round(this.viewport.offset.x+t.point.x*this.viewport.scale),s=Math.round(this.viewport.offset.y+t.point.y*this.viewport.scale);switch(this.ctx.fillStyle=n.fillColor,this.ctx.strokeStyle=n.strokeColor,this.ctx.lineWidth=n.strokeWidth,this.ctx.beginPath(),n.shape){case"square":this.ctx.rect(e-i,s-i,2*i,2*i);break;case"diamond":this.ctx.moveTo(e,s-i),this.ctx.lineTo(e+i,s),this.ctx.lineTo(e,s+i),this.ctx.lineTo(e-i,s),this.ctx.closePath();break;default:this.ctx.arc(e,s,i,0,2*Math.PI)}this.ctx.fill(),n.strokeWidth>0&&this.ctx.stroke()})}drawCurrentPolygon(){const t=this.annotationManager.operate.data,e=this.annotationManager.tempPolygonPoint;if(this.ctx.beginPath(),t.forEach((t,e)=>{const n=Math.round(this.viewport.offset.x+t.point.x*this.viewport.scale),i=Math.round(this.viewport.offset.y+t.point.y*this.viewport.scale);0===e?this.ctx.moveTo(n,i):this.ctx.lineTo(n,i)}),e){const t=Math.round(this.viewport.offset.x+e.x*this.viewport.scale),n=Math.round(this.viewport.offset.y+e.y*this.viewport.scale);this.ctx.lineTo(t,n)}if(this.ctx.stroke(),this.drawPolygonVertices(t,this.annotationManager.getVertexStyle()),e){const t=this.annotationManager.getVertexStyle(),n=Math.round(this.viewport.offset.x+e.x*this.viewport.scale),i=Math.round(this.viewport.offset.y+e.y*this.viewport.scale),s=t.size/2;switch(this.ctx.fillStyle=t.fillColor,this.ctx.strokeStyle=t.strokeColor,this.ctx.lineWidth=t.strokeWidth,this.ctx.beginPath(),t.shape){case"square":this.ctx.rect(n-s,i-s,2*s,2*s);break;case"diamond":this.ctx.moveTo(n,i-s),this.ctx.lineTo(n+s,i),this.ctx.lineTo(n,i+s),this.ctx.lineTo(n-s,i),this.ctx.closePath();break;default:this.ctx.arc(n,i,s,0,2*Math.PI)}this.ctx.fill(),t.strokeWidth>0&&this.ctx.stroke()}}drawSelectionHighlight(){const t=this.annotationManager.selectedAnnotation;if(!t)return;const e=this.annotationManager.recordList[t.index];if(!e)return;const n=this.annotationManager.selectionStyle;this.ctx.save(),"rect"===e.type?this.drawRectSelectionHighlight(e.data[0],n):"polygon"===e.type&&this.drawPolygonSelectionHighlight(e.data,n),this.ctx.restore()}drawRectSelectionHighlight(t,e){const n=this.viewport.offset.x+t.start.x*this.viewport.scale,i=this.viewport.offset.y+t.start.y*this.viewport.scale,s=t.width*this.viewport.scale,a=t.height*this.viewport.scale;this.ctx.fillStyle=e.fillColor,this.ctx.fillRect(n,i,s,a),this.ctx.strokeStyle=e.strokeColor,this.ctx.lineWidth=2,this.ctx.setLineDash([5,5]),this.ctx.strokeRect(n,i,s,a);const o=e.handleSize;this.ctx.fillStyle=e.handleColor,this.ctx.setLineDash([]),this.ctx.fillRect(n-o/2,i-o/2,o,o),this.ctx.fillRect(n+s-o/2,i-o/2,o,o),this.ctx.fillRect(n-o/2,i+a-o/2,o,o),this.ctx.fillRect(n+s-o/2,i+a-o/2,o,o)}drawPolygonSelectionHighlight(t,e){this.ctx.beginPath(),t.forEach((t,e)=>{const n=this.viewport.offset.x+t.point.x*this.viewport.scale,i=this.viewport.offset.y+t.point.y*this.viewport.scale;0===e?this.ctx.moveTo(n,i):this.ctx.lineTo(n,i)}),this.ctx.closePath(),this.ctx.fillStyle=e.fillColor,this.ctx.fill(),this.ctx.strokeStyle=e.strokeColor,this.ctx.lineWidth=2,this.ctx.setLineDash([5,5]),this.ctx.stroke();const n=e.handleSize;this.ctx.fillStyle=e.handleColor,this.ctx.setLineDash([]),t.forEach(t=>{const e=Math.round(this.viewport.offset.x+t.point.x*this.viewport.scale),i=Math.round(this.viewport.offset.y+t.point.y*this.viewport.scale);this.ctx.fillRect(e-n/2,i-n/2,n,n)})}drawTextAnnotations(){const t=this.textManager.textStyle;this.textManager.textAnnotations.forEach((e,n)=>{if(this.textManager.editingTextIndex===n&&this.textManager.textInput&&"none"!==this.textManager.textInput.style.display)return;if(!e.text)return;const i=e.style||{font:t.font,color:t.color,backgroundColor:t.backgroundColor},s=this.viewport.offset.x+e.position.x*this.viewport.scale,a=this.viewport.offset.y+e.position.y*this.viewport.scale,o=this.textManager.selectedTextIndex===n;this.ctx.font=i.font;const r=this.ctx.measureText(e.text),h=r.actualBoundingBoxAscent||.8*e.height,l=h+(r.actualBoundingBoxDescent||.2*e.height),c=t.borderRadius,d=s-t.padding,g=e.width+2*t.padding,x=l+2*t.padding,f=a-h-t.padding;this.ctx.fillStyle=i.backgroundColor,this.ctx.beginPath(),this.ctx.roundRect(d,f,g,x,c),this.ctx.fill(),o&&(this.ctx.strokeStyle=t.selectedBorderColor,this.ctx.lineWidth=2,this.ctx.setLineDash([5,5]),this.ctx.beginPath(),this.ctx.roundRect(d,f,g,x,c),this.ctx.stroke(),this.ctx.setLineDash([])),this.ctx.fillStyle=i.color,this.ctx.fillText(e.text,s,a)})}drawForExport(t,e,n,i){t.drawImage(e,0,0,n,i),this.annotationManager.recordList.forEach(e=>{const n=this.annotationManager.getAnnotationStyle(e);if(t.strokeStyle=n.strokeColor,t.lineWidth=n.lineWidth,"rect"===e.type){const n=e.data[0];t.strokeRect(n.start.x,n.start.y,n.width,n.height)}else if("polygon"===e.type&&"fullfilled"===e.status){t.beginPath(),e.data.forEach((e,n)=>{0===n?t.moveTo(e.point.x,e.point.y):t.lineTo(e.point.x,e.point.y)});const i=e.data[0];t.lineTo(i.point.x,i.point.y),t.stroke(),e.data.forEach(e=>{t.beginPath(),t.arc(e.point.x,e.point.y,4,0,2*Math.PI),t.fillStyle=n.strokeColor,t.fill()})}}),this.textManager.textAnnotations.forEach(e=>{const n=this.textManager.textStyle,i=e.style||{font:n.font,color:n.color,backgroundColor:n.backgroundColor};t.font=i.font;const s=t.measureText(e.text),a=s.actualBoundingBoxAscent||.8*e.height,o=a+(s.actualBoundingBoxDescent||.2*e.height),r=e.position.x-n.padding,h=e.width+2*n.padding,l=o+2*n.padding,c=e.position.y-a-n.padding,d=n.borderRadius;t.fillStyle=i.backgroundColor,t.roundRect?(t.roundRect(r,c,h,l,d),t.fill()):t.fillRect(r,c,h,l),t.fillStyle=i.color,t.fillText(e.text,e.position.x,e.position.y)})}}class d{constructor(t,e,n,i,s,a,o,r,h){this.canvas=t,this.viewport=e,this.annotationManager=n,this.textManager=i,this.renderer=s,this.getDrawType=a,this.getBgImage=o,this.renderCallback=r,this.setDrawTypeCallback=h,this.isDragging=!1,this.dragStart={x:0,y:0},this.justFinishedMove=!1,this.justDeselected=!1}handleWheel(t){if(t.preventDefault(),!this.getBgImage())return;const e=t.deltaY>0?-.02:.02,n=this.viewport.scale,i=Math.max(this.viewport.minScale,Math.min(n+e,this.viewport.maxScale)),s=this.canvas.getBoundingClientRect(),a=t.clientX-s.left,o=t.clientY-s.top,r=(a-this.viewport.offset.x)/n,h=(o-this.viewport.offset.y)/n;this.viewport.scale=i;const l=r*i+this.viewport.offset.x,c=h*i+this.viewport.offset.y;this.viewport.offset.x+=a-l,this.viewport.offset.y+=o-c,this.viewport.calculateBoundaries(),this.viewport.constrainViewport(),this.viewport.isInitialScale=this.viewport.scale===this.viewport.initialScale,this.renderCallback()}handleMouseDown(t){var e,n;if(0!==t.button)return;const i=this.getDrawType(),s=this.getBgImage();if(this.annotationManager.selectedAnnotation&&s){const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY),n=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(n)return this.annotationManager.startResizing(n,e),void this.renderCallback()}if("text"===i){const e=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==e)return this.textManager.selectedTextIndex===e?this.textManager.startMoving(t,e):(this.annotationManager.deselectAnnotation(),this.textManager.selectTextAnnotation(e)),void this.renderCallback();if(s){const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY),n=this.annotationManager.getAnnotationAtPoint(e);if("rect"===(null==n?void 0:n.type))return this.annotationManager.selectAnnotation(n.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback();const i=this.annotationManager.getAnnotationAtPoint(e);if("polygon"===(null==i?void 0:i.type))return this.annotationManager.selectAnnotation(i.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback();if(this.annotationManager.selectedAnnotation)return this.annotationManager.deselectAnnotation(),void this.renderCallback()}return null!==this.textManager.selectedTextIndex?(this.textManager.deselectTextAnnotation(),void this.renderCallback()):void this.handleTextModeClick(t)}if(!s)return;const a=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);if("rect"===i){if("rect"===(null===(e=this.annotationManager.selectedAnnotation)||void 0===e?void 0:e.type)){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e)return this.annotationManager.startResizing(e,a),void this.renderCallback();const n=this.annotationManager.getAnnotationAtPoint(a);return n&&"rect"===n.type?(this.annotationManager.selectAnnotation(n.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback()):(this.annotationManager.deselectAnnotation(),this.justDeselected=!0,void this.renderCallback())}const n=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==n)return this.textManager.selectedTextIndex===n?this.textManager.startMoving(t,n):(this.annotationManager.deselectAnnotation(),this.textManager.selectTextAnnotation(n)),void this.renderCallback();const i=this.annotationManager.getAnnotationAtPoint(a);if("polygon"===(null==i?void 0:i.type))return this.annotationManager.selectAnnotation(i.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback();const s=this.annotationManager.getAnnotationAtPoint(a);if("rect"===(null==s?void 0:s.type))return this.annotationManager.selectAnnotation(s.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback();this.annotationManager.startRectDrawing(a)}else if("polygon"===i){if("polygon"===(null===(n=this.annotationManager.selectedAnnotation)||void 0===n?void 0:n.type)){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e&&"polygon-vertex"===e.type)return this.annotationManager.startResizing(e,a),void this.renderCallback();const n=this.annotationManager.getAnnotationAtPoint(a);return n&&"polygon"===n.type?(this.annotationManager.selectAnnotation(n.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback()):(this.annotationManager.deselectAnnotation(),this.justDeselected=!0,void this.renderCallback())}const e=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==e)return this.textManager.selectedTextIndex===e?this.textManager.startMoving(t,e):(this.annotationManager.deselectAnnotation(),this.textManager.selectTextAnnotation(e)),void this.renderCallback();const i=this.annotationManager.getAnnotationAtPoint(a);if("rect"===(null==i?void 0:i.type))return this.annotationManager.selectAnnotation(i.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback();const s=this.annotationManager.getAnnotationAtPoint(a);if("polygon"===(null==s?void 0:s.type))return this.annotationManager.selectAnnotation(s.index),this.annotationManager.startMovingAnnotation(t),void this.renderCallback();this.annotationManager.isDrawing?this.checkPolygonPointValid(a)&&this.annotationManager.addPolygonPoint(a):this.annotationManager.startPolygonDrawing(a),this.annotationManager.updatePolygonTempPoint(a),this.renderCallback()}else if("drag"===i){if(this.annotationManager.selectedAnnotation){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e)return this.annotationManager.startResizing(e,a),void this.renderCallback()}const e=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==e)return this.textManager.selectedTextIndex===e?this.textManager.startMoving(t,e):(this.annotationManager.deselectAnnotation(),this.textManager.selectTextAnnotation(e)),void this.renderCallback();if(this.handleRectModeClick(t,a))return;if(this.handlePolygonModeClick(t,a))return;this.isDragging=!0,this.dragStart={x:t.clientX,y:t.clientY},this.canvas.style.cursor="grabbing"}else""===i&&this.handleNoModeClick(t,a)}checkTextClickInDragMode(t){return!!this.textManager.checkTextClickForMove(t).handled&&(this.canvas.style.cursor="grabbing",!0)}handleTextModeClick(t){const e=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==e)return this.textManager.startMoving(t,e),this.canvas.style.cursor="grabbing",this.renderCallback(),!0;const n=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);return this.textManager.addTextAnnotation(n.x,n.y),!0}handleRectModeClick(t,e){var n,i;if("rect"===(null===(n=this.annotationManager.selectedAnnotation)||void 0===n?void 0:n.type)){const n=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(n)return this.annotationManager.startResizing(n,e),this.renderCallback(),!0}const s=this.annotationManager.getAnnotationAtPoint(e);return s&&"rect"===s.type?(this.annotationManager.selectAnnotation(s.index),this.annotationManager.startMovingAnnotation(t),this.renderCallback(),!0):"rect"===(null===(i=this.annotationManager.selectedAnnotation)||void 0===i?void 0:i.type)&&(this.annotationManager.deselectAnnotation(),this.renderCallback(),!0)}handlePolygonModeClick(t,e){var n,i;if("polygon"===(null===(n=this.annotationManager.selectedAnnotation)||void 0===n?void 0:n.type)){const n=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(n&&"polygon-vertex"===n.type)return this.annotationManager.startResizing(n,e),this.renderCallback(),!0}const s=this.annotationManager.getAnnotationAtPoint(e);return s&&"polygon"===s.type?(this.annotationManager.selectAnnotation(s.index),this.annotationManager.startMovingAnnotation(t),this.renderCallback(),!0):"polygon"===(null===(i=this.annotationManager.selectedAnnotation)||void 0===i?void 0:i.type)&&(this.annotationManager.deselectAnnotation(),this.renderCallback(),!0)}handleRectModeClickForMove(t,e){const n=this.annotationManager.getAnnotationAtPoint(e);return!(!n||"rect"!==n.type)&&(this.annotationManager.selectAnnotation(n.index),this.annotationManager.startMovingAnnotation(t),this.renderCallback(),!0)}handlePolygonModeClickForMove(t,e){const n=this.annotationManager.getAnnotationAtPoint(e);return!(!n||"polygon"!==n.type)&&(this.annotationManager.selectAnnotation(n.index),this.annotationManager.startMovingAnnotation(t),this.renderCallback(),!0)}handleNoModeClick(t,e){const n=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==n)return this.textManager.selectedTextIndex===n?this.textManager.startMoving(t,n):(this.annotationManager.deselectAnnotation(),this.textManager.selectTextAnnotation(n)),void this.renderCallback();this.textManager.deselectTextAnnotation();const i=this.annotationManager.getAnnotationAtPoint(e);i?(this.annotationManager.selectAnnotation(i.index),this.annotationManager.startMovingAnnotation(t),this.renderCallback()):this.annotationManager.deselectAnnotation()}checkPolygonPointValid(t){const e=this.annotationManager.operate.data.length>0?this.annotationManager.operate.data[this.annotationManager.operate.data.length-1]:null;return!e||Math.abs(e.point.x-t.x)>5||Math.abs(e.point.y-t.y)>5}handleMouseMove(t){if(!this.getBgImage())return;const e=this.getDrawType();if(this.isDragging&&"drag"===e){const e=t.clientX-this.dragStart.x,n=t.clientY-this.dragStart.y;return this.viewport.updateOffset(e,n),this.dragStart={x:t.clientX,y:t.clientY},void this.renderCallback()}if(this.textManager.isTextMoving&&null!==this.textManager.editingTextIndex)this.textManager.moveAnnotation(t)&&this.renderCallback();else if((this.annotationManager.isMovingAnnotation||this.annotationManager.isResizing)&&this.annotationManager.selectedAnnotation)this.handleAnnotationMove(t);else{if(this.annotationManager.isMovingAnnotation||this.annotationManager.isResizing||this.textManager.isTextMoving||this.updateCursor(t),this.annotationManager.isDrawing&&"rect"===this.annotationManager.operate.type){const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);this.annotationManager.updateRectDrawing(e),this.renderCallback()}if("polygon"===e&&this.annotationManager.isDrawing){const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);this.annotationManager.updatePolygonTempPoint(e),this.renderCallback()}}}handleAnnotationMove(t){var e,n;if(this.annotationManager.isResizing&&this.annotationManager.activeHandle){const i=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);return"rect"===(null===(e=this.annotationManager.selectedAnnotation)||void 0===e?void 0:e.type)?this.annotationManager.resizeRect(i):"polygon"===(null===(n=this.annotationManager.selectedAnnotation)||void 0===n?void 0:n.type)&&this.annotationManager.resizePolygon(i),void this.renderCallback()}if(!this.annotationManager.isMovingAnnotation||!this.annotationManager.selectedAnnotation)return;const i=(t.clientX-this.annotationManager.annotationMoveStart.x)/this.viewport.scale,s=(t.clientY-this.annotationManager.annotationMoveStart.y)/this.viewport.scale;this.annotationManager.moveSelectedAnnotation(i,s),this.annotationManager.annotationMoveStart={x:t.clientX,y:t.clientY},this.renderCallback()}updateCursor(t){const e=this.getDrawType();if("text"===e){if(this.getBgImage()){const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);if(this.annotationManager.selectedAnnotation){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e)return void("rect-corner"===e.type?this.canvas.style.cursor=0===e.index||3===e.index?"nwse-resize":"nesw-resize":this.canvas.style.cursor="crosshair")}if(this.annotationManager.getAnnotationAtPoint(e))return void(this.canvas.style.cursor="move")}const e=this.textManager.checkTextClick(t.offsetX,t.offsetY);return void(this.canvas.style.cursor=null!==e?"move":"crosshair")}"rect"!==e?"polygon"!==e?"drag"!==e?""!==e?this.canvas.style.cursor="default":this.updateNoModeCursor(t):this.updateDragModeCursor(t):this.updatePolygonModeCursor(t):this.updateRectModeCursor(t)}updateRectModeCursor(t){var e;if("rect"===(null===(e=this.annotationManager.selectedAnnotation)||void 0===e?void 0:e.type)){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e)return void(this.canvas.style.cursor=0===e.index||3===e.index?"nwse-resize":"nesw-resize")}const n=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);this.annotationManager.getAnnotationAtPoint(n)?this.canvas.style.cursor="move":null===this.textManager.checkTextClick(t.offsetX,t.offsetY)?this.canvas.style.cursor="crosshair":this.canvas.style.cursor="move"}updatePolygonModeCursor(t){var e;if("polygon"===(null===(e=this.annotationManager.selectedAnnotation)||void 0===e?void 0:e.type)){if(this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY))return void(this.canvas.style.cursor="crosshair")}const n=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);this.annotationManager.getAnnotationAtPoint(n)?this.canvas.style.cursor="move":null===this.textManager.checkTextClick(t.offsetX,t.offsetY)?this.canvas.style.cursor="crosshair":this.canvas.style.cursor="move"}updateDragModeCursor(t){if(this.annotationManager.selectedAnnotation){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e)return void("rect-corner"===e.type?this.canvas.style.cursor=0===e.index||3===e.index?"nwse-resize":"nesw-resize":this.canvas.style.cursor="crosshair")}if(null!==this.textManager.checkTextClick(t.offsetX,t.offsetY))return void(this.canvas.style.cursor="move");const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY),n=this.annotationManager.getAnnotationAtPoint(e);this.canvas.style.cursor=n?"move":"grab"}updateNoModeCursor(t){if(this.annotationManager.selectedAnnotation){const e=this.annotationManager.getHandleAtPoint(t.offsetX,t.offsetY);if(e)return void("rect-corner"===e.type?this.canvas.style.cursor=0===e.index||3===e.index?"nwse-resize":"nesw-resize":this.canvas.style.cursor="crosshair")}if(null!==this.textManager.checkTextClick(t.offsetX,t.offsetY))return void(this.canvas.style.cursor="move");const e=this.viewport.toImageCoordinates(t.offsetX,t.offsetY),n=this.annotationManager.getAnnotationAtPoint(e);this.canvas.style.cursor=n?"move":"default"}handleMouseUp(t){if(this.isDragging){this.isDragging=!1;const t=this.getDrawType();return void(this.canvas.style.cursor="drag"===t?"grab":"default")}if(this.textManager.isTextMoving){this.justFinishedMove=!0,this.textManager.finishMoving();const t=this.getDrawType();"drag"===t?this.canvas.style.cursor="grab":""===t?this.canvas.style.cursor="move":(this.textManager.editingTextIndex=null,this.canvas.style.cursor="default")}(this.annotationManager.isMovingAnnotation||this.annotationManager.isResizing)&&(this.justFinishedMove=!0,this.annotationManager.finishMovingAnnotation(),this.canvas.style.cursor=this.annotationManager.selectedAnnotation?"move":"default"),this.annotationManager.isDrawing&&"rect"===this.annotationManager.operate.type&&(this.annotationManager.finishRectDrawing(),this.renderCallback())}handleMouseLeave(t){(this.isDragging||this.textManager.isTextMoving||this.annotationManager.isResizing||this.annotationManager.isMovingAnnotation)&&this.handleMouseUp(t)}handleDoubleClick(t){if(!this.getBgImage())return;this.justFinishedMove=!1;const e=this.textManager.checkTextClick(t.offsetX,t.offsetY);if(null!==e)return void this.textManager.startEditing(e);if("polygon"===this.getDrawType()){if(this.annotationManager.isDrawing&&this.annotationManager.operate.data.length<=2)return;this.annotationManager.finishPolygonDrawing(),this.renderCallback()}}handleClick(t){var e;const n=this.getDrawType();if(!this.getBgImage()||"polygon"!==n||this.isDragging||this.annotationManager.isMovingAnnotation||this.annotationManager.isResizing||this.textManager.isTextMoving)return;if(this.justDeselected)return void(this.justDeselected=!1);if("polygon"===(null===(e=this.annotationManager.selectedAnnotation)||void 0===e?void 0:e.type))return;const i=this.viewport.toImageCoordinates(t.offsetX,t.offsetY);this.checkPolygonPointValid(i)&&(this.annotationManager.isDrawing?this.annotationManager.addPolygonPoint(i):this.annotationManager.startPolygonDrawing(i)),this.annotationManager.updatePolygonTempPoint(i),this.renderCallback()}handleKeyDown(t){if("z"===t.key&&(t.ctrlKey||t.metaKey))return this.annotationManager.withdraw()||this.textManager.withdraw(),void this.renderCallback();if(" "===t.key)return this.viewport.resetToInitial(),void this.renderCallback();if("Escape"!==t.key){if("Delete"===t.key||"Backspace"===t.key){if(null!==this.textManager.editingTextIndex)return this.textManager.removeTextAnnotation(this.textManager.editingTextIndex),void this.renderCallback();if(null!==this.textManager.selectedTextIndex)return this.textManager.deleteSelectedTextAnnotation(),void this.renderCallback();this.annotationManager.selectedAnnotation&&(this.annotationManager.deleteSelectedAnnotation(),this.renderCallback())}}else this.annotationManager.isDrawing?(this.annotationManager.cancelDrawing(),this.renderCallback()):this.annotationManager.selectedAnnotation&&(this.annotationManager.deselectAnnotation(),this.renderCallback())}}class g{constructor(t){this.canvas=document.createElement("canvas"),this.bgImage=null,this.bgImageSrc="",this.bgImageExt="jpeg",this.drawType="";const{drawType:e,useEvents:n=!0,id:i="container",annotationColor:s,lineStyle:a,vertexStyle:o,textStyle:g}=t,x=document.getElementById(i);if(!x)throw new Error(`Container with id "${i}" not found`);this.container=x,"absolute"!==x.style.position&&"fixed"!==x.style.position&&"relative"!==x.style.position&&(x.style.position="relative"),this.ctx=this.canvas.getContext("2d"),this.viewport=new r,this.viewport.setSize(x.clientWidth,x.clientHeight),this.canvas.width=this.viewport.width,this.canvas.height=this.viewport.height,this.canvas.style.width=`${this.viewport.width}px`,this.canvas.style.height=`${this.viewport.height}px`,this.canvas.style.cursor="default",this.canvas.style.display="block",x.appendChild(this.canvas),this.annotationManager=new h(this.viewport),s&&this.annotationManager.setColorConfig(s),a&&this.annotationManager.setLineStyle(a),o&&this.annotationManager.setVertexStyle(o),this.textManager=new l(this.viewport,this.container,this.ctx,()=>this.render()),g&&this.textManager.setTextStyle(g),this.renderer=new c(this.ctx,this.viewport,this.annotationManager,this.textManager,this.canvas),this.eventHandler=new d(this.canvas,this.viewport,this.annotationManager,this.textManager,this.renderer,()=>this.drawType,()=>this.bgImage,()=>this.render(),t=>this.setDrawType(t)),this.events={wheel:t=>this.eventHandler.handleWheel(t),mousedown:t=>this.eventHandler.handleMouseDown(t),mousemove:t=>this.eventHandler.handleMouseMove(t),mouseup:t=>this.eventHandler.handleMouseUp(t),mouseleave:t=>this.eventHandler.handleMouseLeave(t),dblclick:t=>this.eventHandler.handleDoubleClick(t),click:t=>this.eventHandler.handleClick(t),keydown:t=>this.eventHandler.handleKeyDown(t)},this.setDrawType(e||""),n&&this.addEventListeners()}setDrawType(t){"polygon"===this.drawType&&this.annotationManager.isDrawing&&this.annotationManager.operate.data.length>0?console.warn("Cannot switch mode while polygon is being drawn. Please complete or cancel the polygon first."):this.drawType!==t&&(this.annotationManager.operate.data=[],this.annotationManager.isDrawing=!1,this.annotationManager.tempPolygonPoint=null,""!==t&&this.deselectAnnotation(),this.annotationManager.operate.type=t,this.drawType=t,this.canvas.style.cursor="drag"===t?"grab":"text"===t?"text":"default")}drawImage(t){this.bgImageSrc=t,this.bgImageExt=i(t);const e=new Image;e.crossOrigin="anonymous",e.src=t,e.onload=()=>{this.bgImage=e,this.viewport.setOriginalSize(e.width,e.height),this.viewport.calculateInitialView(),this.render()}}render(){this.renderer.render(this.bgImage)}clear(t=""){this.annotationManager.clear(),this.textManager.clearTextAnnotations(),this.setDrawType(t),this.render()}withdraw(){const t=this.annotationManager.withdraw(),e=this.textManager.withdraw();(t||e)&&this.render()}resize(){const t=this.canvas.parentElement;t&&(this.viewport.setSize(t.clientWidth,t.clientHeight),this.canvas.width=this.viewport.width,this.canvas.height=this.viewport.height,this.canvas.style.width=`${this.viewport.width}px`,this.canvas.style.height=`${this.viewport.height}px`,this.render())}addEventListeners(){this.canvas.addEventListener("mousedown",this.events.mousedown),this.canvas.addEventListener("mousemove",this.events.mousemove),this.canvas.addEventListener("mouseup",this.events.mouseup),this.canvas.addEventListener("mouseleave",this.events.mouseleave),this.canvas.addEventListener("wheel",this.events.wheel),this.canvas.addEventListener("contextmenu",t=>t.preventDefault()),document.addEventListener("keydown",this.events.keydown),this.handleClickEventListener()}handleClickEventListener(){let t=null,e=0;this.canvas.addEventListener("click",n=>{if(this.eventHandler.isDragging||this.annotationManager.isMovingAnnotation||this.annotationManager.isResizing||this.textManager.isTextMoving)return;const i=this.eventHandler.justFinishedMove;i&&(this.eventHandler.justFinishedMove=!1),e++,1===e?t=window.setTimeout(()=>{1!==e||i||this.events.click(n),e=0,t=null},300):2===e&&(this.eventHandler.justFinishedMove=!1,this.events.dblclick(n),e=0,null!==t&&(clearTimeout(t),t=null))})}changeScale(t){if(!this.bgImage)return;const e=o(this.viewport.scale,t),n=this.viewport.width/2,i=this.viewport.height/2,s=this.viewport.scale+e;this.viewport.updateScale(s,n,i),this.render()}clearCanvas(t=!1,e=!0){this.annotationManager.clear(),this.textManager.clearTextAnnotations(),this.textManager.deselectTextAnnotation(),!t&&e&&(this.bgImage=null,this.bgImageSrc="",this.viewport.setOriginalSize(0,0),this.viewport.reset()),this.ctx.clearRect(0,0,this.viewport.width,this.viewport.height),t&&this.bgImage&&this.render(),"drag"===this.drawType&&(this.canvas.style.cursor="default")}clearAnnotations(){this.clearCanvas(!0,!1)}getAnnotations(){return this.annotationManager.getAnnotations()}getTextAnnotations(){return this.textManager.getTextAnnotations()}exportAnnotationImage(){return new Promise((t,e)=>{if(!this.bgImage)return void e(new Error("No background image loaded"));const n=document.createElement("canvas");n.width=this.viewport.originalWidth,n.height=this.viewport.originalHeight;const i=n.getContext("2d");if(!i)return void e(new Error("Could not create offscreen canvas context"));const s=new Image;s.src=this.bgImageSrc,s.crossOrigin="anonymous",s.onload=()=>{try{this.renderer.drawForExport(i,s,this.viewport.originalWidth,this.viewport.originalHeight);const e=n.toDataURL(`image/${this.bgImageExt}`,.7);t(e)}catch(n){this.exportCurrentViewImage().then(t).catch(t=>e(new Error(`Both methods failed: ${t.message}`)))}},s.onerror=()=>{e(new Error("Failed to load image for export"))}})}exportCurrentViewImage(){return new Promise((t,e)=>{try{t(this.canvas.toDataURL(`image/${this.bgImageExt}`))}catch(t){e(new Error(`Failed to export current view: ${t.message}`))}})}base64ToFile(t,e){return s(t,e)}static base64ToBlob(t){return a(t)}addTextAnnotation(t,e,n=""){this.textManager.addTextAnnotation(t,e,n),this.render()}updateTextAnnotation(t,e){this.textManager.updateTextAnnotation(t,e)&&this.render()}moveTextAnnotation(t,e,n){this.textManager.moveTextAnnotation(t,e,n)&&this.render()}removeTextAnnotation(t){this.textManager.removeTextAnnotation(t)&&this.render()}clearTextAnnotations(){this.textManager.clearTextAnnotations(),this.render()}selectAnnotation(t){this.annotationManager.selectAnnotation(t)&&this.render()}deselectAnnotation(){this.annotationManager.deselectAnnotation(),this.textManager.deselectTextAnnotation(),this.textManager.editingTextIndex=null,this.render()}getSelectedAnnotation(){const t=this.annotationManager.getSelectedAnnotation();if(t)return t;const e=this.textManager.getSelectedTextAnnotation();return e?{index:e.index,type:"text",data:e.data}:null}deleteSelectedAnnotation(){if(null!==this.textManager.selectedTextIndex)return this.textManager.deleteSelectedTextAnnotation(),void this.render();this.annotationManager.deleteSelectedAnnotation()&&this.render()}moveSelectedAnnotation(t,e){this.annotationManager.moveSelectedAnnotation(t,e)&&this.render()}setAnnotationColor(t){this.annotationManager.setColorConfig(t),this.render()}getAnnotationColor(){return this.annotationManager.getColorConfig()}updateSelectedAnnotationStyle(){const t=this.annotationManager.selectedAnnotation;if(!t)return!1;const e=this.annotationManager.recordList[t.index];return!!e&&(e.style=this.annotationManager.getCurrentStyle(),this.render(),!0)}setLineStyle(t){this.annotationManager.setLineStyle(t),this.render()}getLineStyle(){return this.annotationManager.getLineStyle()}setVertexStyle(t){this.annotationManager.setVertexStyle(t),this.render()}getVertexStyle(){return this.annotationManager.getVertexStyle()}setTextStyle(t){this.textManager.setTextStyle(t),this.render()}setTextInputStyle(t){this.textManager.setInputStyle(t)}setTextSelectionStyle(t){this.textManager.setSelectionStyle(t),this.render()}destroy(){this.textManager.destroy(),this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas)}}t.AnnotationManager=h,t.Drawer=g,t.EventHandler=d,t.Renderer=c,t.TextAnnotationManager=l,t.ViewportManager=r,t.base64ToBlob=a,t.base64ToFile=s,t.default=g,t.getImageTypeFromUrl=i,t.getZoomDelta=o,t.isPointInPolygon=n,t.isPointInRect=e,Object.defineProperty(t,"__esModule",{value:!0})}),"undefined"!=typeof ImageAnnotationDrawer&&ImageAnnotationDrawer.default&&(ImageAnnotationDrawer=ImageAnnotationDrawer.default);