image-annotation-drawer 1.0.0

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 ADDED
@@ -0,0 +1,3333 @@
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