kfb-view 2.1.19 → 2.2.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.
@@ -5,6 +5,8 @@ import {
5
5
  import {ViewerCommon} from '../common/common';
6
6
  import * as Tools from '../../tool';
7
7
  import {Combination} from '../../tool/Combination';
8
+ import {EVENT_CANCEL_SELECT_LABEL} from '../../const/event';
9
+ import * as EVENTS from '../../const/event';
8
10
 
9
11
  /**
10
12
  * 用来显示标注线的canvas
@@ -25,6 +27,7 @@ export class Shape extends ViewerCommon {
25
27
  this[mark] = new Tools[mark];
26
28
  });
27
29
  this.combination = new Combination();
30
+ this.delaytimer = [];
28
31
  }
29
32
 
30
33
  /**
@@ -32,15 +35,6 @@ export class Shape extends ViewerCommon {
32
35
  * @param {Object} e
33
36
  */
34
37
  onCanvasKey(e) {
35
- const {originalEvent} = e;
36
- e.preventDefaultAction = true;
37
- if (originalEvent.code === 'Escape') {
38
- this.labelList.forEach((item) => {
39
- item.select = false;
40
- item.show = true;
41
- });
42
- this.change();
43
- }
44
38
  }
45
39
 
46
40
  /**
@@ -57,35 +51,26 @@ export class Shape extends ViewerCommon {
57
51
  */
58
52
  change() {
59
53
  this.clearCanvas();
54
+ const bounds = this.viewport.getBounds();
60
55
  // 区域标注列表
61
56
  const regionLabelList = [];
62
- const polygonLabelList = [];
63
- this.labelList.forEach(
64
- (item) => {
65
- if (item.tool === POLYGON) {
66
- polygonLabelList.push(item);
67
- }
68
- if (REGION_TYPES.includes(item.tool) || item.isClose && item.tool ===
69
- POLYGON) {
70
- regionLabelList.push({
71
- ...item,
72
- child: [],
73
- self: item,
74
- });
75
- }
76
- });
57
+ const labelList = this.labelList.filter((item) => {
58
+ if (item.show === false) return false;
59
+ if (!this[item.tool]) return false;
60
+ if (REGION_TYPES.includes(item.tool) || item.isClose && item.tool ===
61
+ POLYGON && item.fillStyle) {
62
+ regionLabelList.push({
63
+ ...item,
64
+ child: [],
65
+ self: item,
66
+ });
67
+ }
68
+ return true;
69
+ });
77
70
  // strokeStyle, lineWidth, fillStyle 相同认为是一个路径下的图形,一起绘制,优化性能
78
71
  const sameFixWidthLabel = {}; // star, flag, star, font lineWidth固定为2
79
72
  const sameNormalLabel = {};
80
- this.labelList.forEach((item) => {
81
- if (item.show === false) return;
82
- if (!this[item.tool]) return;
83
- if (!this.isInCanvas(item.region)) return;
84
- /* const point = this.imageToViewerElementCoordinates(item.points[0].x,
85
- item.points[0].y);
86
- if ((point.x > this.canvas.width ||
87
- point.y > this.canvas.height ||
88
- point.x < 0 || point.y < 0)) return;*/
73
+ labelList.forEach((item) => {
89
74
  const key = `${item.strokeStyle ?? ''}_${item.lineWidth ??
90
75
  ''}_${item.fillStyle ?? ''}`;
91
76
  const regionKey = `${item.strokeStyle ?? ''}_${item.lineWidth ?? ''}`;
@@ -110,11 +95,43 @@ export class Shape extends ViewerCommon {
110
95
  parent.child.push(item);
111
96
  }
112
97
  });
98
+ if (this.delaytimer) {
99
+ clearTimeout(this.delaytimer);
100
+ this.delaytimer = undefined;
101
+ }
102
+ this.deepDrawLabel(sameFixWidthLabel, sameNormalLabel, 1, bounds);
103
+ regionLabelList.forEach((item) => {
104
+ if (!this.isInCanvas(item.region, bounds)) return false;
105
+ this.combination.setContent(this.canvas, item);
106
+ this.combination.draw({
107
+ points: this.imageToViewerElementPoints(item.points),
108
+ tool: item.tool,
109
+ }, item.child.map(({points, tool}) => ({
110
+ points: this.imageToViewerElementPoints(points),
111
+ tool,
112
+ })), this.viewport.getRotation());
113
+ });
114
+ }
115
+
116
+ deepDrawLabel(sameFixWidthLabel, sameNormalLabel, count, bounds) {
117
+ let currentDrawCount = 0; // 当前已绘制数量
113
118
  const ctx = this.canvas.getContext('2d');
114
- Object.values(sameFixWidthLabel).forEach((labels) => {
119
+ const remainFixLabel = {};
120
+ Object.keys(sameFixWidthLabel).forEach((key) => {
121
+ const labels = sameFixWidthLabel[key];
115
122
  ctx.beginPath();
116
123
  labels.forEach((item) => {
117
- this.drawLabel(item);
124
+ if (currentDrawCount >= 5000) { // 一次最多绘制5000个标注,多余标注延迟绘制
125
+ if (remainFixLabel[key]) {
126
+ remainFixLabel[key].push(item);
127
+ } else {
128
+ remainFixLabel[key] = [item];
129
+ }
130
+ } else {
131
+ if (!this.isInCanvas(item.region, bounds)) return false;
132
+ this.drawLabel(item);
133
+ }
134
+ currentDrawCount++;
118
135
  });
119
136
  const firstLabel = labels?.[0];
120
137
  if (firstLabel?.fillStyle) {
@@ -124,45 +141,38 @@ export class Shape extends ViewerCommon {
124
141
  ctx.strokeStyle = firstLabel.strokeStyle;
125
142
  ctx.stroke();
126
143
  });
127
- Object.values(sameNormalLabel).forEach((labels) => {
144
+ const remainNormalLabel = {};
145
+ Object.keys(sameNormalLabel).forEach((key) => {
146
+ const labels = sameNormalLabel[key];
128
147
  ctx.beginPath();
129
148
  labels.forEach((item) => {
130
- this.drawLabel(item);
149
+ if (currentDrawCount >= 5000) { // 一次最多绘制5000个标注,多余标注延迟绘制
150
+ if (remainNormalLabel[key]) {
151
+ remainNormalLabel[key].push(item);
152
+ } else {
153
+ remainNormalLabel[key] = [item];
154
+ }
155
+ } else {
156
+ if (!this.isInCanvas(item.region, bounds)) return false;
157
+ this.drawLabel(item);
158
+ }
159
+ currentDrawCount++;
131
160
  });
132
161
  const firstLabel = labels?.[0];
133
162
  ctx.lineWidth = firstLabel.lineWidth;
134
163
  ctx.strokeStyle = firstLabel.strokeStyle;
135
164
  ctx.stroke();
136
165
  });
137
-
138
- regionLabelList.forEach((item) => {
139
- if (item.show === false) return;
140
- if (!this[item.tool]) return;
141
- if (!this.isInCanvas(item.region)) return;
142
- /* const point = this.imageToViewerElementCoordinates(item.points[0].x,
143
- item.points[0].y);
144
- if ((point.x > this.canvas.width ||
145
- point.y > this.canvas.height ||
146
- point.x < 0 || point.y < 0)) return;*/
147
- this.combination.setContent(this.canvas, item);
148
- this.combination.draw({
149
- points: this.imageToViewerElementPoints(item.points),
150
- tool: item.tool,
151
- }, item.child.map(({points, tool}) => ({
152
- points: this.imageToViewerElementPoints(points),
153
- tool,
154
- })), this.viewport.getRotation());
155
- });
156
-
157
- polygonLabelList.forEach((item) => {
158
- if (item.show === false) return;
159
- if (!this[item.tool]) return;
160
- if (!this.isInCanvas(item.region)) return;
161
- const point = this.imageToViewerElementCoordinates(item.points[0].x,
162
- item.points[0].y);
163
- this[item.tool].setContent(this.canvas, item);
164
- this[item.tool].drawPoint(point);
165
- });
166
+ if (Object.keys(remainFixLabel).length === 0 &&
167
+ Object.keys(remainNormalLabel).length === 0) {
168
+ console.log(`end deep draw label, draw count: ${count}`);
169
+ this.delaytimer = undefined;
170
+ } else {
171
+ this.delaytimer = setTimeout(() => {
172
+ this.deepDrawLabel(remainFixLabel, remainNormalLabel, count + 1,
173
+ bounds);
174
+ }, 100);
175
+ }
166
176
  }
167
177
 
168
178
  drawLabel(item) {
@@ -220,8 +220,7 @@ export class Tailoring extends ViewerCommon {
220
220
  */
221
221
  onCanvasKey(e) {
222
222
  const {originalEvent} = e;
223
- e.preventDefaultAction = true;
224
- if (originalEvent.code === 'Escape') {
223
+ if (originalEvent.key === 'Escape') {
225
224
  this.stopTailoring();
226
225
  }
227
226
  }
@@ -1,3 +1,4 @@
1
+
1
2
  export const EVENT_START_PAINTING = 'start-painting';
2
3
  export const EVENT_IN_PAINTING = 'in-painting';
3
4
  export const EVENT_END_PAINTING = 'end-painting';
@@ -13,4 +14,6 @@ export const EVENT_CACHE_BACK = 'cache-back';
13
14
  export const EVENT_CACHE_FORWARD = 'cache-forward';
14
15
  export const EVENT_HIDDEN_LABEL = 'hidden-label';
15
16
  export const EVENT_DELETE_LABEL = 'delete-label';
17
+ export const EVENT_DELETE_POLYGON_POINT = 'delete-polygon-point';
18
+ export const EVENT_ADD_POLYGON_POINT = 'add-polygon-point';
16
19
  export const EVENT_NAVIGATOR_VESTIGE = 'navigator-vestige';
@@ -23,7 +23,6 @@ export class LabelModel {
23
23
  this.resize = data.resize; // 是否可拖动大小
24
24
  this.isROI = data.isROI ?? false; // 是否是ROI
25
25
  this.isClose = data.isClose ?? true; // 是否是闭合标注
26
- this.showStartPoint = data.showStartPoint ?? true; // 是否默认显示开始点,只针对曲线
27
26
  this.select = data.select ?? false; // 是否是选中状态
28
27
  this.show = data.show ?? true; // 是否显示
29
28
  this.__other__ = data.__other__ || {}; // 其他信息
package/src/tool/Brush.js CHANGED
@@ -42,7 +42,7 @@ class Brush {
42
42
  * @param {Object} point
43
43
  * @param {Object} config 参数,参见thumb
44
44
  */
45
- drawPoint(point, config) {
45
+ drawThumb(point, config) {
46
46
  this.thumb.draw(point, config);
47
47
  }
48
48
 
@@ -74,6 +74,7 @@ class Combination extends Brush {
74
74
  ctx.drawOval(0, 0, Math.abs(region.width / 2),
75
75
  Math.abs(region.height / 2), true);
76
76
  }
77
+ ctx.closePath();
77
78
  ctx.restore();
78
79
  });
79
80
  if (this.options.fillStyle) {
@@ -16,7 +16,8 @@ class Polygon extends Brush {
16
16
  ctx.lineWidth = this.options.lineWidth;
17
17
  ctx.strokeStyle = this.options.strokeStyle;
18
18
  ctx.moveTo(points[0].x, points[0].y);
19
- points.forEach((point) => {
19
+ points.forEach((point, index) => {
20
+ if (index === 0) return;
20
21
  ctx.lineTo(point.x, point.y);
21
22
  });
22
23
  if (this.options.isClose) {
@@ -27,21 +28,10 @@ class Polygon extends Brush {
27
28
  }
28
29
  }
29
30
 
30
- drawPoint(point) {
31
- if (this.options.showStartPoint) {
32
- const ctx = this.canvas.getContext('2d');
33
- ctx.beginPath();
34
- ctx.fillStyle = '#0000008d';
35
- ctx.fillRect(point.x - 20, point.y - 25, 40, 20);
36
- ctx.beginPath();
37
- ctx.font = `14px Arial`;
38
- ctx.fillStyle = '#ffffff';
39
- ctx.fillText('起点', point.x - 15, point.y - 10);
40
- this.thumb.draw(point, {
41
- thumbRadius: this.options.thumbRadius ?? 5,
42
- strokeStyle: this.options.thumbColor,
43
- });
44
- }
31
+ drawPoints(points, config) {
32
+ points.forEach((point) => {
33
+ this.drawThumb(point, config);
34
+ });
45
35
  }
46
36
  }
47
37
 
package/src/tool/Thumb.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const ThumbConfig = {
2
2
  lineWidth: 2,
3
- thumbRadius: 5,
3
+ radius: 5,
4
4
  fillStyle: 'rgba(255, 255, 255, 1)',
5
5
  strokeStyle: '#01d0b0',
6
6
  };
@@ -51,7 +51,7 @@ class Thumb {
51
51
  config = this.getConfig(config);
52
52
  ctx.beginPath();
53
53
  ctx.lineWidth = config.lineWidth;
54
- ctx.arc(x, y, config.thumbRadius, 0, Math.PI * 2, !1);
54
+ ctx.arc(x, y, config.radius, 0, Math.PI * 2, !1);
55
55
  ctx.fillStyle = config.fillStyle;
56
56
  ctx.fill();
57
57
  ctx.strokeStyle = config.strokeStyle;
@@ -132,9 +132,8 @@ function isPointInPolygon(p, poly) {
132
132
  if ((sy < py && ty >= py) || (sy >= py && ty < py)) {
133
133
  // 线段上与射线 Y 坐标相同的点的 X 坐标
134
134
  const x = sx + (py - sy) * (tx - sx) / (ty - sy);
135
-
136
- // 点在多边形的边上
137
- if (x === px) {
135
+ // 点在多边形的边上,8像素的偏差
136
+ if (Math.abs(x - px) < 10) {
138
137
  return true;
139
138
  }
140
139
 
@@ -149,6 +148,29 @@ function isPointInPolygon(p, poly) {
149
148
  return flag;
150
149
  }
151
150
 
151
+ /**
152
+ * 判断一个点是否在一条线段上
153
+ * @param {Object} p
154
+ * @param {Object[]} points
155
+ * @return {boolean}
156
+ */
157
+ function isPointInLine(p, points) {
158
+ const startPoint = points[0];
159
+ const endPoint = points[1];
160
+ // 判断线段两端点是否在射线两侧
161
+ if ((startPoint.y < p.y && endPoint.y >= p.y) ||
162
+ (startPoint.y >= p.y && endPoint.y < p.y)) {
163
+ // 线段上与射线 Y 坐标相同的点的 X 坐标
164
+ const x = startPoint.x + (p.y - startPoint.y) *
165
+ (endPoint.x - startPoint.x) / (endPoint.y - startPoint.y);
166
+ // 点在多边形的边上,8像素的偏差
167
+ if (Math.abs(x - p.x) < 8) {
168
+ return true;
169
+ }
170
+ }
171
+ return false;
172
+ }
173
+
152
174
  /**
153
175
  * points转换成Region
154
176
  * @param {Object[]} points - 绘制的点
@@ -366,6 +388,7 @@ export {
366
388
  pointsToRegion,
367
389
  regionToPoint,
368
390
  isPointInPolygon,
391
+ isPointInLine,
369
392
  isNegNumber,
370
393
  baseNumber,
371
394
  acreage,
package/src/view.js CHANGED
@@ -6,7 +6,7 @@ import {pointsToRegion} from './util/calculate';
6
6
  import * as COMPONENTS from './const/component';
7
7
  import * as EVENTS from './const/event';
8
8
  import * as Components from './components';
9
- import {MARKS} from './const/mark';
9
+ import {MARKS, POLYGON} from './const/mark';
10
10
  import {LabelModel} from './model/label.model';
11
11
 
12
12
  /**
@@ -37,9 +37,14 @@ export default class KfbView extends EventEmitter {
37
37
  * @param {Object} config.area 交互区域,触发点击、拖动等事件
38
38
  * @param {boolean=} config.area.disabled 是否禁用area
39
39
  * @param {boolean=} config.area.drag 是否允许拖动
40
- * @param {boolean=} config.area.thumb 选中区域时是否显示标注点
41
- * @param {boolean=} config.area.thumbRadius 选中区域标注点的半径
42
- * @param {boolean=} config.area.thumbColor 选中区域标注点的颜色
40
+ * @param {Object} config.thumb 选中区域时标注点配置
41
+ * @param {boolean=} config.thumb.show 选中区域时是否标注点
42
+ * @param {number=} config.thumb.radius 选中区域标注点的半径
43
+ * @param {String=} config.thumb.color 选中区域标注点的颜色
44
+ * @param {String=} config.thumb.bgColor 选中区域标注点的背景颜色
45
+ * @param {number=} config.thumb.activeRadius 标注点激活状态下的半径
46
+ * @param {boolean=} config.thumb.activeColor 标注点激活状态下颜色
47
+ * @param {boolean=} config.thumb.activeBgColor 标注点激活状态下背景颜色
43
48
  * @param {Object} config.rotation 旋转图像参数
44
49
  * @param {boolean=} config.rotation.disabled 是否禁用旋转
45
50
  * @param {Object} config.tailoring 裁剪图像参数
@@ -254,7 +259,18 @@ function initComponentsOptions(kv, type) {
254
259
  viewer: kv.viewer,
255
260
  cache: kv.cache,
256
261
  canvas: createCanvas(kv),
257
- options: {...config[type], ...pxConversion},
262
+ options: {
263
+ ...config[type], ...pxConversion, thumb: config.thumb ? {
264
+ radius: 5,
265
+ activeRadius: 7,
266
+ activeBgColor: '#01d0b0',
267
+ ...config.thumb,
268
+ } : {
269
+ radius: 5,
270
+ activeRadius: 7,
271
+ activeBgColor: '#01d0b0',
272
+ },
273
+ },
258
274
  };
259
275
  }
260
276
 
@@ -287,8 +303,7 @@ function initEvent(kv) {
287
303
  e.preventDefaultAction = true;
288
304
  kv.tailoring.onCanvasDrag(e.position);
289
305
  } else if (kv.area?.movePoint) {
290
- e.preventDefaultAction = true;
291
- kv.area.onCanvasDrag(e.position);
306
+ kv.area.onCanvasDrag(e.position, e);
292
307
  kv.change();
293
308
  }
294
309
  });
@@ -308,17 +323,27 @@ function initEvent(kv) {
308
323
  e.preventDefaultAction = true;
309
324
  const {originalEvent} = e;
310
325
  handlerCacheEvent(originalEvent, kv);
311
- handlerLabelEvent(originalEvent, kv);
312
- kv.shape?.onCanvasKey?.(e);
313
- kv.area?.onCanvasKey?.(e);
314
- kv.board?.onCanvasKey?.(e);
315
- kv.tailoring?.onCanvasKey?.(e);
316
- kv.rotation?.onCanvasKey?.(e);
326
+ if (kv.board?.isInDraw) {
327
+ kv.board?.onCanvasKey?.(e);
328
+ } else if (kv.rotation?.isInRotation) {
329
+ kv.rotation?.onCanvasKey?.(e);
330
+ } else if (kv.tailoring?.isInTailoring) {
331
+ kv.tailoring?.onCanvasKey?.(e);
332
+ } else {
333
+ handlerLabelEvent(originalEvent, kv);
334
+ kv.area?.change?.(e);
335
+ kv.shape?.change?.(e);
336
+ }
317
337
  }, 100));
318
338
 
319
339
  kv.viewer.addHandler('canvas-double-click', (e) => {
320
- kv.area?.onCanvasDblClick?.(e.position);
321
- kv.tailoring?.onCanvasDblClick?.(e.position);
340
+ if (kv.board?.isInDraw) {
341
+ kv.board.onCanvasDblClick(e.position);
342
+ } else if (kv.tailoring?.isInTailoring) {
343
+ kv.tailoring?.onCanvasDblClick?.(e.position);
344
+ } else {
345
+ kv.area?.onCanvasDblClick?.(e.position);
346
+ }
322
347
  });
323
348
 
324
349
  kv.viewer.addHandler('resize', (e) => {
@@ -331,6 +356,43 @@ function initEvent(kv) {
331
356
  kv.board.onCanvasMove(e);
332
357
  }
333
358
  }, true, kv.$el);
359
+
360
+ let isRightDown = false;
361
+ let dragPosition;
362
+
363
+ kv.viewer.canvas.addEventListener('contextmenu', function(event) {
364
+ if (kv.board?.isInDraw) {
365
+ event.preventDefault();
366
+ }
367
+ });
368
+
369
+ kv.viewer.addHandler('canvas-nonprimary-press', function(e) {
370
+ console.log('nonprimary press');
371
+ if (e.button === 2) {
372
+ isRightDown = true;
373
+ }
374
+ dragPosition = e.position.clone();
375
+ });
376
+
377
+ kv.viewer.addHandler('canvas-nonprimary-release', function(e) {
378
+ console.log('nonprimary release');
379
+ if (e.button === 2) {
380
+ isRightDown = false;
381
+ }
382
+ });
383
+
384
+ new openSeadragon.MouseTracker({
385
+ element: kv.viewer.canvas,
386
+ moveHandler: function(event) {
387
+ if (isRightDown && kv.board?.isInDraw) {
388
+ console.log('nonprimary drag');
389
+ let delta = event.position.minus(dragPosition);
390
+ dragPosition = event.position.clone();
391
+ kv.viewer.viewport.panBy(
392
+ kv.viewer.viewport.deltaPointsFromPixels(delta.negate()));
393
+ }
394
+ },
395
+ });
334
396
  }
335
397
 
336
398
  function handlerCacheEvent(e, kv) {
@@ -349,17 +411,36 @@ function handlerCacheEvent(e, kv) {
349
411
  }
350
412
 
351
413
  function handlerLabelEvent(e, kv) {
414
+ const index = kv.labelList.findIndex((item) => item.select);
415
+ const label = kv.labelList[index];
352
416
  if (e.key === 'h') {
353
- const label = kv.labelList.find((label) => label.select);
354
417
  if (!label) return;
355
418
  label.show = false;
356
419
  kv.$emit(EVENTS.EVENT_HIDDEN_LABEL, label);
357
- }
358
- if (e.key === 'Delete' || e.key === 'Backspace') {
359
- const index = kv.labelList.findIndex((label) => label.select);
360
- if (!~index) return;
361
- kv.$emit(EVENTS.EVENT_DELETE_LABEL, kv.labelList[index]);
362
- kv.labelList.splice(index, 1);
420
+ } else if (e.key === 'Delete' || e.key === 'Backspace') {
421
+ if (!label) return;
422
+ if (kv.area?.movePoint?.label?.tool === POLYGON) {
423
+ const movePoint = kv.area.movePoint;
424
+ movePoint.label.points.splice(movePoint.position, 1);
425
+ if (movePoint.label.points.length === 0) {
426
+ kv.$emit(EVENTS.EVENT_DELETE_LABEL, label);
427
+ kv.labelList.splice(index, 1);
428
+ } else {
429
+ kv.$emit(EVENTS.EVENT_DELETE_POLYGON_POINT, movePoint.label);
430
+ }
431
+ kv.area.movePoint = undefined;
432
+ } else {
433
+ kv.$emit(EVENTS.EVENT_DELETE_LABEL, label);
434
+ kv.labelList.splice(index, 1);
435
+ }
436
+ } else if (e.key === 'Escape') {
437
+ if (label) {
438
+ kv.$emit(EVENTS.EVENT_CANCEL_SELECT_LABEL, label);
439
+ }
440
+ kv.labelList.forEach((item) => {
441
+ item.select = false;
442
+ item.show = true;
443
+ });
363
444
  }
364
445
  }
365
446