image-annotation-drawer 1.0.0 → 1.0.1

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