@cornerstonejs/tools 1.38.1 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/cjs/index.d.ts +2 -2
  2. package/dist/cjs/index.js +3 -2
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/tools/VolumeRotateMouseWheelTool.js +2 -2
  5. package/dist/cjs/tools/VolumeRotateMouseWheelTool.js.map +1 -1
  6. package/dist/cjs/tools/annotation/LivewireContourTool.d.ts +44 -0
  7. package/dist/cjs/tools/annotation/LivewireContourTool.js +442 -0
  8. package/dist/cjs/tools/annotation/LivewireContourTool.js.map +1 -0
  9. package/dist/cjs/tools/index.d.ts +2 -1
  10. package/dist/cjs/tools/index.js +3 -1
  11. package/dist/cjs/tools/index.js.map +1 -1
  12. package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +10 -0
  13. package/dist/cjs/utilities/BucketQueue.d.ts +20 -0
  14. package/dist/cjs/utilities/BucketQueue.js +83 -0
  15. package/dist/cjs/utilities/BucketQueue.js.map +1 -0
  16. package/dist/cjs/utilities/livewire/LiveWirePath.d.ts +16 -0
  17. package/dist/cjs/utilities/livewire/LiveWirePath.js +64 -0
  18. package/dist/cjs/utilities/livewire/LiveWirePath.js.map +1 -0
  19. package/dist/cjs/utilities/livewire/LivewireScissors.d.ts +37 -0
  20. package/dist/cjs/utilities/livewire/LivewireScissors.js +281 -0
  21. package/dist/cjs/utilities/livewire/LivewireScissors.js.map +1 -0
  22. package/dist/cjs/utilities/math/vec2/liangBarksyClip.d.ts +1 -1
  23. package/dist/esm/index.js +2 -2
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/tools/VolumeRotateMouseWheelTool.js +2 -2
  26. package/dist/esm/tools/VolumeRotateMouseWheelTool.js.map +1 -1
  27. package/dist/esm/tools/annotation/LivewireContourTool.js +439 -0
  28. package/dist/esm/tools/annotation/LivewireContourTool.js.map +1 -0
  29. package/dist/esm/tools/index.js +2 -1
  30. package/dist/esm/tools/index.js.map +1 -1
  31. package/dist/esm/utilities/BucketQueue.js +79 -0
  32. package/dist/esm/utilities/BucketQueue.js.map +1 -0
  33. package/dist/esm/utilities/livewire/LiveWirePath.js +60 -0
  34. package/dist/esm/utilities/livewire/LiveWirePath.js.map +1 -0
  35. package/dist/esm/utilities/livewire/LivewireScissors.js +277 -0
  36. package/dist/esm/utilities/livewire/LivewireScissors.js.map +1 -0
  37. package/dist/types/index.d.ts +2 -2
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/tools/VolumeRotateMouseWheelTool.d.ts.map +1 -1
  40. package/dist/types/tools/annotation/LivewireContourTool.d.ts +45 -0
  41. package/dist/types/tools/annotation/LivewireContourTool.d.ts.map +1 -0
  42. package/dist/types/tools/index.d.ts +2 -1
  43. package/dist/types/tools/index.d.ts.map +1 -1
  44. package/dist/types/types/ToolSpecificAnnotationTypes.d.ts +10 -0
  45. package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
  46. package/dist/types/utilities/BucketQueue.d.ts +21 -0
  47. package/dist/types/utilities/BucketQueue.d.ts.map +1 -0
  48. package/dist/types/utilities/livewire/LiveWirePath.d.ts +17 -0
  49. package/dist/types/utilities/livewire/LiveWirePath.d.ts.map +1 -0
  50. package/dist/types/utilities/livewire/LivewireScissors.d.ts +38 -0
  51. package/dist/types/utilities/livewire/LivewireScissors.d.ts.map +1 -0
  52. package/dist/types/utilities/math/vec2/liangBarksyClip.d.ts +1 -1
  53. package/dist/umd/index.js +1 -1
  54. package/dist/umd/index.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/index.ts +2 -0
  57. package/src/tools/VolumeRotateMouseWheelTool.ts +3 -2
  58. package/src/tools/annotation/LivewireContourTool.ts +799 -0
  59. package/src/tools/index.ts +2 -0
  60. package/src/types/ToolSpecificAnnotationTypes.ts +12 -0
  61. package/src/utilities/BucketQueue.ts +154 -0
  62. package/src/utilities/livewire/LiveWirePath.ts +131 -0
  63. package/src/utilities/livewire/LivewireScissors.ts +582 -0
@@ -0,0 +1,799 @@
1
+ import { glMatrix, mat4, vec3 } from 'gl-matrix';
2
+ import { AnnotationTool } from '../base';
3
+
4
+ import {
5
+ getEnabledElement,
6
+ eventTarget,
7
+ triggerEvent,
8
+ utilities as csUtils,
9
+ StackViewport,
10
+ VolumeViewport,
11
+ } from '@cornerstonejs/core';
12
+ import type { Types } from '@cornerstonejs/core';
13
+ import {
14
+ addAnnotation,
15
+ getAnnotations,
16
+ removeAnnotation,
17
+ } from '../../stateManagement/annotation/annotationState';
18
+ import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
19
+ import {
20
+ drawHandles as drawHandlesSvg,
21
+ drawPolyline as drawPolylineSvg,
22
+ } from '../../drawingSvg';
23
+ import { state } from '../../store';
24
+ import { Events } from '../../enums';
25
+ import { resetElementCursor } from '../../cursors/elementCursor';
26
+ import {
27
+ EventTypes,
28
+ ToolHandle,
29
+ PublicToolProps,
30
+ ToolProps,
31
+ SVGDrawingHelper,
32
+ } from '../../types';
33
+ import {
34
+ math,
35
+ viewportFilters,
36
+ triggerAnnotationRenderForViewportIds,
37
+ } from '../../utilities';
38
+ import { LivewireContourAnnotation } from '../../types/ToolSpecificAnnotationTypes';
39
+ import {
40
+ AnnotationCompletedEventDetail,
41
+ AnnotationModifiedEventDetail,
42
+ } from '../../types/EventTypes';
43
+ import { StyleSpecifier } from '../../types/AnnotationStyle';
44
+
45
+ import { LivewireScissors } from '../../utilities/livewire/LivewireScissors';
46
+ import { LivewirePath } from '../../utilities/livewire/LiveWirePath';
47
+
48
+ const { getViewportIdsWithToolToRender } = viewportFilters;
49
+ const CLICK_CLOSE_CURVE_SQR_DIST = 10 ** 2; // px
50
+
51
+ class LivewireContourTool extends AnnotationTool {
52
+ public static toolName: string;
53
+ private scissors: LivewireScissors;
54
+
55
+ touchDragCallback: any;
56
+ mouseDragCallback: any;
57
+ editData: {
58
+ annotation: LivewireContourAnnotation;
59
+ viewportIdsToRender: Array<string>;
60
+ handleIndex?: number;
61
+ newAnnotation?: boolean;
62
+ hasMoved?: boolean;
63
+ lastCanvasPoint?: Types.Point2;
64
+ confirmedPath?: LivewirePath;
65
+ currentPath?: LivewirePath;
66
+ closed?: boolean;
67
+ worldToSlice?: (point: Types.Point3) => Types.Point2;
68
+ sliceToWorld?: (point: Types.Point2) => Types.Point3;
69
+ } | null;
70
+ isDrawing: boolean;
71
+ isHandleOutsideImage = false;
72
+
73
+ constructor(
74
+ toolProps: PublicToolProps = {},
75
+ defaultToolProps: ToolProps = {
76
+ supportedInteractionTypes: ['Mouse', 'Touch'],
77
+ configuration: {
78
+ preventHandleOutsideImage: false,
79
+ },
80
+ }
81
+ ) {
82
+ super(toolProps, defaultToolProps);
83
+ }
84
+
85
+ /**
86
+ * Based on the current position of the mouse and the current imageId to create
87
+ * a CircleROI Annotation and stores it in the annotationManager
88
+ *
89
+ * @param evt - EventTypes.NormalizedMouseEventType
90
+ * @returns The annotation object.
91
+ *
92
+ */
93
+ addNewAnnotation = (
94
+ evt: EventTypes.InteractionEventType
95
+ ): LivewireContourAnnotation => {
96
+ const eventDetail = evt.detail;
97
+ const { currentPoints, element } = eventDetail;
98
+ const { world: worldPos, canvas: canvasPos } = currentPoints;
99
+
100
+ const enabledElement = getEnabledElement(element);
101
+ const { viewport, renderingEngine } = enabledElement;
102
+
103
+ this.isDrawing = true;
104
+
105
+ const camera = viewport.getCamera();
106
+ const { viewPlaneNormal, viewUp } = camera;
107
+
108
+ const referencedImageId = this.getReferencedImageId(
109
+ viewport,
110
+ worldPos,
111
+ viewPlaneNormal,
112
+ viewUp
113
+ );
114
+
115
+ const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
116
+ const defaultActor = viewport.getDefaultActor();
117
+
118
+ if (!defaultActor || !csUtils.isImageActor(defaultActor)) {
119
+ throw new Error('Default actor must be an image actor');
120
+ }
121
+
122
+ const viewportImageData = viewport.getImageData();
123
+ const { imageData: vtkImageData } = viewportImageData;
124
+ let worldToSlice: (point: Types.Point3) => Types.Point2;
125
+ let sliceToWorld: (point: Types.Point2) => Types.Point3;
126
+ let scalarData;
127
+ let width;
128
+ let height;
129
+
130
+ if (viewport instanceof StackViewport) {
131
+ scalarData = viewportImageData.scalarData;
132
+ width = viewportImageData.dimensions[0];
133
+ height = viewportImageData.dimensions[1];
134
+
135
+ // Method only to simplify the code making stack and volume viewports code
136
+ // similar and avoiding `if(stack)/else` whenever a coordinate needs to be
137
+ // transformed because `worldToSlice` in this case returns the same IJK
138
+ // coordinate from index space.
139
+ worldToSlice = (point: Types.Point3) => {
140
+ const ijkPoint = csUtils.transformWorldToIndex(vtkImageData, point);
141
+ return [ijkPoint[0], ijkPoint[1]];
142
+ };
143
+
144
+ // Method only to simplify the code making stack and volume viewports code
145
+ // similar and avoiding `if(stack)/else` whenever a coordinate needs to be
146
+ // transformed because `sliceToWorld` in this case receives the same IJK
147
+ // coordinate from index space.
148
+ sliceToWorld = (point: Types.Point2) =>
149
+ csUtils.transformIndexToWorld(vtkImageData, [point[0], point[1], 0]);
150
+ } else if (viewport instanceof VolumeViewport) {
151
+ const sliceImageData = csUtils.getCurrentVolumeViewportSlice(viewport);
152
+ const { sliceToIndexMatrix, indexToSliceMatrix } = sliceImageData;
153
+
154
+ worldToSlice = (point: Types.Point3) => {
155
+ const ijkPoint = csUtils.transformWorldToIndex(vtkImageData, point);
156
+ const slicePoint = vec3.transformMat4(
157
+ [0, 0, 0],
158
+ ijkPoint,
159
+ indexToSliceMatrix
160
+ );
161
+
162
+ return [slicePoint[0], slicePoint[1]];
163
+ };
164
+
165
+ sliceToWorld = (point: Types.Point2) => {
166
+ const ijkPoint = vec3.transformMat4(
167
+ [0, 0, 0],
168
+ [point[0], point[1], 0],
169
+ sliceToIndexMatrix
170
+ ) as Types.Point3;
171
+
172
+ return csUtils.transformIndexToWorld(vtkImageData, ijkPoint);
173
+ };
174
+
175
+ scalarData = sliceImageData.scalarData;
176
+ width = sliceImageData.width;
177
+ height = sliceImageData.height;
178
+ } else {
179
+ throw new Error('Viewport not supported');
180
+ }
181
+
182
+ const { voiRange } = viewport.getProperties();
183
+ const startPos = worldToSlice(worldPos);
184
+
185
+ this.scissors = LivewireScissors.createInstanceFromRawPixelData(
186
+ scalarData,
187
+ width,
188
+ height,
189
+ voiRange
190
+ );
191
+
192
+ this.scissors.startSearch(startPos);
193
+
194
+ const confirmedPath = new LivewirePath();
195
+ const currentPath = new LivewirePath();
196
+
197
+ confirmedPath.addPoint(startPos);
198
+ confirmedPath.addControlPoint(startPos);
199
+
200
+ const annotation: LivewireContourAnnotation = {
201
+ highlighted: true,
202
+ invalidated: true,
203
+ metadata: {
204
+ toolName: this.getToolName(),
205
+ viewPlaneNormal: <Types.Point3>[...viewPlaneNormal],
206
+ viewUp: <Types.Point3>[...viewUp],
207
+ FrameOfReferenceUID,
208
+ referencedImageId,
209
+ },
210
+ data: {
211
+ polyline: [],
212
+ handles: {
213
+ points: [[...worldPos]],
214
+ activeHandleIndex: null,
215
+ },
216
+ },
217
+ };
218
+
219
+ addAnnotation(annotation, element);
220
+
221
+ const viewportIdsToRender = getViewportIdsWithToolToRender(
222
+ element,
223
+ this.getToolName()
224
+ );
225
+
226
+ this.editData = {
227
+ annotation,
228
+ viewportIdsToRender,
229
+ newAnnotation: true,
230
+ hasMoved: false,
231
+ lastCanvasPoint: canvasPos,
232
+ confirmedPath: confirmedPath,
233
+ currentPath: currentPath,
234
+ closed: false,
235
+ worldToSlice,
236
+ sliceToWorld,
237
+ };
238
+
239
+ this._activateDraw(element);
240
+ evt.preventDefault();
241
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
242
+
243
+ return annotation;
244
+ };
245
+
246
+ /**
247
+ * It returns if the canvas point is near the provided annotation in the provided
248
+ * element or not. A proximity is passed to the function to determine the
249
+ * proximity of the point to the annotation in number of pixels.
250
+ *
251
+ * @param element - HTML Element
252
+ * @param annotation - Annotation
253
+ * @param canvasCoords - Canvas coordinates
254
+ * @param proximity - Proximity to tool to consider
255
+ * @returns Boolean, whether the canvas point is near tool
256
+ */
257
+ isPointNearTool = (
258
+ element: HTMLDivElement,
259
+ annotation: LivewireContourAnnotation,
260
+ canvasCoords: Types.Point2,
261
+ proximity: number
262
+ ): boolean => {
263
+ const enabledElement = getEnabledElement(element);
264
+ const { viewport } = enabledElement;
265
+ const proximitySquared = proximity * proximity;
266
+ const canvasPoints = annotation.data.polyline.map((p) =>
267
+ viewport.worldToCanvas(p)
268
+ );
269
+
270
+ let startPoint = canvasPoints[canvasPoints.length - 1];
271
+
272
+ for (let i = 0; i < canvasPoints.length; i++) {
273
+ const endPoint = canvasPoints[i];
274
+ const distanceToPointSquared = math.lineSegment.distanceToPointSquared(
275
+ startPoint,
276
+ endPoint,
277
+ canvasCoords
278
+ );
279
+
280
+ if (distanceToPointSquared <= proximitySquared) {
281
+ return true;
282
+ }
283
+
284
+ startPoint = endPoint;
285
+ }
286
+
287
+ return false;
288
+ };
289
+
290
+ toolSelectedCallback = (
291
+ evt: EventTypes.InteractionEventType,
292
+ annotation: LivewireContourAnnotation
293
+ ): void => {
294
+ const eventDetail = evt.detail;
295
+ const { element } = eventDetail;
296
+
297
+ annotation.highlighted = true;
298
+
299
+ const viewportIdsToRender = getViewportIdsWithToolToRender(
300
+ element,
301
+ this.getToolName()
302
+ );
303
+
304
+ this.editData = {
305
+ annotation,
306
+ viewportIdsToRender,
307
+ };
308
+
309
+ const enabledElement = getEnabledElement(element);
310
+ const { renderingEngine } = enabledElement;
311
+
312
+ this._activateModify(element);
313
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
314
+ evt.preventDefault();
315
+ };
316
+
317
+ handleSelectedCallback = (
318
+ evt: EventTypes.InteractionEventType,
319
+ annotation: LivewireContourAnnotation,
320
+ handle: ToolHandle
321
+ ): void => {
322
+ const eventDetail = evt.detail;
323
+ const { element } = eventDetail;
324
+ const { data } = annotation;
325
+
326
+ annotation.highlighted = true;
327
+
328
+ const { points } = data.handles;
329
+ const handleIndex = points.findIndex((p) => p === handle);
330
+
331
+ // Find viewports to render on drag.
332
+ const viewportIdsToRender = getViewportIdsWithToolToRender(
333
+ element,
334
+ this.getToolName()
335
+ );
336
+
337
+ this.editData = {
338
+ annotation,
339
+ viewportIdsToRender,
340
+ handleIndex,
341
+ };
342
+ this._activateModify(element);
343
+
344
+ const enabledElement = getEnabledElement(element);
345
+ const { renderingEngine } = enabledElement;
346
+
347
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
348
+
349
+ evt.preventDefault();
350
+ };
351
+
352
+ _endCallback = (evt: EventTypes.InteractionEventType): void => {
353
+ const eventDetail = evt.detail;
354
+ const { element } = eventDetail;
355
+
356
+ const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
357
+ const { data } = annotation;
358
+
359
+ data.handles.activeHandleIndex = null;
360
+
361
+ this._deactivateModify(element);
362
+ this._deactivateDraw(element);
363
+
364
+ resetElementCursor(element);
365
+
366
+ const enabledElement = getEnabledElement(element);
367
+ const { renderingEngine } = enabledElement;
368
+
369
+ if (
370
+ this.isHandleOutsideImage &&
371
+ this.configuration.preventHandleOutsideImage
372
+ ) {
373
+ removeAnnotation(annotation.annotationUID);
374
+ }
375
+
376
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
377
+
378
+ if (newAnnotation) {
379
+ const eventType = Events.ANNOTATION_COMPLETED;
380
+ const eventDetail: AnnotationCompletedEventDetail = {
381
+ annotation,
382
+ };
383
+
384
+ triggerEvent(eventTarget, eventType, eventDetail);
385
+ }
386
+
387
+ this.editData = null;
388
+ this.scissors = null;
389
+ this.isDrawing = false;
390
+ };
391
+
392
+ private _mouseDownCallback = (evt: EventTypes.InteractionEventType): void => {
393
+ const doubleClick = evt.type === Events.MOUSE_DOUBLE_CLICK;
394
+ const { annotation, viewportIdsToRender, worldToSlice, sliceToWorld } =
395
+ this.editData;
396
+
397
+ if (this.editData.closed) {
398
+ return;
399
+ }
400
+
401
+ const eventDetail = evt.detail;
402
+ const { element } = eventDetail;
403
+ const { currentPoints } = eventDetail;
404
+ const { canvas: canvasPos, world: worldPos } = currentPoints;
405
+ const enabledElement = getEnabledElement(element);
406
+ const { viewport, renderingEngine } = enabledElement;
407
+ const controlPoints = this.editData.currentPath.getControlPoints();
408
+ let closePath = controlPoints.length >= 2 && doubleClick;
409
+ let addNewPoint = true;
410
+
411
+ // Check if user clicked on the first point to close the curve
412
+ if (controlPoints.length >= 2) {
413
+ const closestHandlePoint = {
414
+ index: -1,
415
+ distSquared: Infinity,
416
+ };
417
+
418
+ // Check if there is a control point close to the cursor
419
+ for (let i = 0, len = controlPoints.length; i < len; i++) {
420
+ const controlPoint = controlPoints[i];
421
+ const worldControlPoint = sliceToWorld(controlPoint);
422
+ const canvasControlPoint = viewport.worldToCanvas(worldControlPoint);
423
+
424
+ const distSquared = math.point.distanceToPointSquared(
425
+ canvasPos,
426
+ canvasControlPoint
427
+ );
428
+
429
+ if (
430
+ distSquared <= CLICK_CLOSE_CURVE_SQR_DIST &&
431
+ distSquared < closestHandlePoint.distSquared
432
+ ) {
433
+ closestHandlePoint.distSquared = distSquared;
434
+ closestHandlePoint.index = i;
435
+ }
436
+ }
437
+
438
+ if (closestHandlePoint.index === 0) {
439
+ addNewPoint = false;
440
+ closePath = true;
441
+ }
442
+ }
443
+
444
+ this.editData.closed = this.editData.closed || closePath;
445
+
446
+ if (addNewPoint) {
447
+ this.editData.confirmedPath = this.editData.currentPath;
448
+
449
+ // Add the current cursor position as a new control point after clicking
450
+ this.editData.confirmedPath.addControlPoint(
451
+ this.editData.currentPath.getLastPoint()
452
+ );
453
+
454
+ // Start a new search starting at the last control point
455
+ this.scissors.startSearch(worldToSlice(worldPos));
456
+ }
457
+
458
+ annotation.invalidated = true;
459
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
460
+
461
+ if (this.editData.closed) {
462
+ // Update the annotation because `editData` will be set to null
463
+ this._updateAnnotation(element, this.editData.confirmedPath);
464
+ this._endCallback(evt);
465
+ }
466
+
467
+ evt.preventDefault();
468
+ };
469
+
470
+ private _mouseMoveCallback = (evt: EventTypes.InteractionEventType): void => {
471
+ const { element, currentPoints } = evt.detail;
472
+ const { world: worldPos, canvas: canvasPos } = currentPoints;
473
+ const { renderingEngine } = getEnabledElement(element);
474
+ const viewportIdsToRender = getViewportIdsWithToolToRender(
475
+ element,
476
+ this.getToolName()
477
+ );
478
+
479
+ this.editData.lastCanvasPoint = canvasPos;
480
+
481
+ const { width: imgWidth, height: imgHeight } = this.scissors;
482
+ const { worldToSlice } = this.editData;
483
+ const slicePoint: Types.Point2 = worldToSlice(worldPos);
484
+
485
+ // Check if the point is inside the bounding box
486
+ if (
487
+ slicePoint[0] < 0 ||
488
+ slicePoint[1] < 0 ||
489
+ slicePoint[0] >= imgWidth ||
490
+ slicePoint[1] >= imgHeight
491
+ ) {
492
+ return;
493
+ }
494
+
495
+ const pathPoints = this.scissors.findPathToPoint(slicePoint);
496
+ const currentPath = new LivewirePath();
497
+
498
+ for (let i = 0, len = pathPoints.length; i < len; i++) {
499
+ currentPath.addPoint(pathPoints[i]);
500
+ }
501
+
502
+ // Merge the "confirmed" path that goes from the first control point to the
503
+ // last one with the current path that goes from the last control point to
504
+ // the cursor point
505
+ currentPath.prependPath(this.editData.confirmedPath);
506
+
507
+ // Store the new path
508
+ this.editData.currentPath = currentPath;
509
+
510
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
511
+ evt.preventDefault();
512
+ };
513
+
514
+ private _dragCallback = (evt: EventTypes.InteractionEventType): void => {
515
+ this.isDrawing = true;
516
+ const eventDetail = evt.detail;
517
+ const { element } = eventDetail;
518
+
519
+ const { annotation, viewportIdsToRender, handleIndex } = this.editData;
520
+ const { data } = annotation;
521
+
522
+ if (handleIndex === undefined) {
523
+ // Drag mode - moving handle
524
+ const { deltaPoints } = eventDetail as EventTypes.MouseDragEventDetail;
525
+ const worldPosDelta = deltaPoints.world;
526
+
527
+ const points = data.polyline;
528
+
529
+ points.forEach((point) => {
530
+ point[0] += worldPosDelta[0];
531
+ point[1] += worldPosDelta[1];
532
+ point[2] += worldPosDelta[2];
533
+ });
534
+ annotation.invalidated = true;
535
+ } else {
536
+ // Move mode - after double click, and mouse move to draw
537
+ const { currentPoints } = eventDetail;
538
+ const worldPos = currentPoints.world;
539
+
540
+ data.handles.points[handleIndex] = [...worldPos];
541
+ annotation.invalidated = true;
542
+ }
543
+
544
+ this.editData.hasMoved = true;
545
+
546
+ const enabledElement = getEnabledElement(element);
547
+ const { renderingEngine } = enabledElement;
548
+
549
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
550
+ };
551
+
552
+ cancel = (element: HTMLDivElement) => {
553
+ // If it is not in mid-draw or mid-modify
554
+ if (!this.isDrawing) {
555
+ return;
556
+ }
557
+
558
+ this.isDrawing = false;
559
+ this._deactivateDraw(element);
560
+ this._deactivateModify(element);
561
+ resetElementCursor(element);
562
+
563
+ const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
564
+
565
+ if (newAnnotation) {
566
+ removeAnnotation(annotation.annotationUID);
567
+ }
568
+
569
+ const enabledElement = getEnabledElement(element);
570
+ const { renderingEngine } = enabledElement;
571
+
572
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
573
+
574
+ this.editData = null;
575
+ this.scissors = null;
576
+ return annotation.annotationUID;
577
+ };
578
+
579
+ /**
580
+ * Triggers an annotation modified event.
581
+ */
582
+ triggerAnnotationModified = (
583
+ annotation: LivewireContourAnnotation,
584
+ enabledElement: Types.IEnabledElement
585
+ ): void => {
586
+ const { viewportId, renderingEngineId } = enabledElement;
587
+ const eventType = Events.ANNOTATION_MODIFIED;
588
+
589
+ const eventDetail: AnnotationModifiedEventDetail = {
590
+ annotation,
591
+ viewportId,
592
+ renderingEngineId,
593
+ };
594
+
595
+ triggerEvent(eventTarget, eventType, eventDetail);
596
+ };
597
+
598
+ private _activateModify = (element) => {
599
+ state.isInteractingWithTool = true;
600
+
601
+ element.addEventListener(Events.MOUSE_UP, this._endCallback);
602
+ element.addEventListener(Events.MOUSE_DRAG, this._dragCallback);
603
+ element.addEventListener(Events.MOUSE_CLICK, this._endCallback);
604
+
605
+ element.addEventListener(Events.TOUCH_END, this._endCallback);
606
+ element.addEventListener(Events.TOUCH_DRAG, this._dragCallback);
607
+ element.addEventListener(Events.TOUCH_TAP, this._endCallback);
608
+ };
609
+
610
+ private _deactivateModify = (element) => {
611
+ state.isInteractingWithTool = false;
612
+
613
+ element.removeEventListener(Events.MOUSE_UP, this._endCallback);
614
+ element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback);
615
+ element.removeEventListener(Events.MOUSE_CLICK, this._endCallback);
616
+
617
+ element.removeEventListener(Events.TOUCH_END, this._endCallback);
618
+ element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback);
619
+ element.removeEventListener(Events.TOUCH_TAP, this._endCallback);
620
+ };
621
+
622
+ private _activateDraw = (element) => {
623
+ state.isInteractingWithTool = true;
624
+
625
+ element.addEventListener(Events.MOUSE_MOVE, this._mouseMoveCallback);
626
+ element.addEventListener(Events.MOUSE_DOWN, this._mouseDownCallback);
627
+ element.addEventListener(
628
+ Events.MOUSE_DOUBLE_CLICK,
629
+ this._mouseDownCallback
630
+ );
631
+
632
+ element.addEventListener(Events.TOUCH_TAP, this._mouseDownCallback);
633
+ };
634
+
635
+ private _deactivateDraw = (element) => {
636
+ state.isInteractingWithTool = false;
637
+
638
+ element.removeEventListener(Events.MOUSE_MOVE, this._mouseMoveCallback);
639
+ element.removeEventListener(Events.MOUSE_DOWN, this._mouseDownCallback);
640
+ element.removeEventListener(
641
+ Events.MOUSE_DOUBLE_CLICK,
642
+ this._mouseDownCallback
643
+ );
644
+
645
+ element.removeEventListener(Events.TOUCH_TAP, this._mouseDownCallback);
646
+ };
647
+
648
+ /**
649
+ * it is used to draw the circleROI annotation in each
650
+ * request animation frame. It calculates the updated cached statistics if
651
+ * data is invalidated and cache it.
652
+ *
653
+ * @param enabledElement - The Cornerstone's enabledElement.
654
+ * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
655
+ */
656
+ renderAnnotation = (
657
+ enabledElement: Types.IEnabledElement,
658
+ svgDrawingHelper: SVGDrawingHelper
659
+ ): boolean => {
660
+ let renderStatus = false;
661
+ const { viewport } = enabledElement;
662
+ const { worldToCanvas } = viewport;
663
+ const { element } = viewport;
664
+
665
+ // If rendering engine has been destroyed while rendering
666
+ if (!viewport.getRenderingEngine()) {
667
+ console.warn('Rendering Engine has been destroyed');
668
+ return renderStatus;
669
+ }
670
+
671
+ let annotations = getAnnotations(this.getToolName(), element);
672
+
673
+ if (!annotations?.length) {
674
+ return renderStatus;
675
+ }
676
+
677
+ annotations = this.filterInteractableAnnotationsForElement(
678
+ element,
679
+ annotations
680
+ );
681
+
682
+ if (!annotations?.length) {
683
+ return renderStatus;
684
+ }
685
+
686
+ const newAnnotation = this.editData?.newAnnotation;
687
+ const styleSpecifier: StyleSpecifier = {
688
+ toolGroupId: this.toolGroupId,
689
+ toolName: this.getToolName(),
690
+ viewportId: enabledElement.viewport.id,
691
+ };
692
+
693
+ // Update the annotation that is in editData (being edited)
694
+ this._updateAnnotation(element, this.editData?.currentPath);
695
+
696
+ for (let i = 0; i < annotations.length; i++) {
697
+ const annotation = annotations[i] as LivewireContourAnnotation;
698
+ const { annotationUID, data } = annotation;
699
+ const { handles } = data;
700
+ const { points } = handles;
701
+
702
+ styleSpecifier.annotationUID = annotationUID;
703
+
704
+ const lineWidth = this.getStyle(
705
+ 'lineWidth',
706
+ styleSpecifier,
707
+ annotation
708
+ ) as number;
709
+ const lineDash = this.getStyle(
710
+ 'lineDash',
711
+ styleSpecifier,
712
+ annotation
713
+ ) as string;
714
+ const color = this.getStyle(
715
+ 'color',
716
+ styleSpecifier,
717
+ annotation
718
+ ) as string;
719
+
720
+ const canvasCoordinates = points.map((p) =>
721
+ worldToCanvas(p)
722
+ ) as Types.Point2[];
723
+
724
+ if (!isAnnotationVisible(annotationUID)) {
725
+ continue;
726
+ }
727
+
728
+ // Render the first control point only when the annotaion is drawn for the
729
+ // first time to make it easier to know where the user needs to click to
730
+ // to close the ROI.
731
+ if (
732
+ newAnnotation &&
733
+ annotation.annotationUID === this.editData?.annotation?.annotationUID
734
+ ) {
735
+ const handleGroupUID = '0';
736
+ drawHandlesSvg(
737
+ svgDrawingHelper,
738
+ annotationUID,
739
+ handleGroupUID,
740
+ [canvasCoordinates[0]],
741
+ {
742
+ color,
743
+ lineDash,
744
+ lineWidth,
745
+ }
746
+ );
747
+ }
748
+
749
+ const canvasPolyline = data.polyline.map((worldPoint) =>
750
+ viewport.worldToCanvas(worldPoint)
751
+ );
752
+
753
+ drawPolylineSvg(
754
+ svgDrawingHelper,
755
+ annotationUID,
756
+ 'polyline',
757
+ canvasPolyline,
758
+ {
759
+ color,
760
+ lineDash,
761
+ lineWidth,
762
+ }
763
+ );
764
+
765
+ renderStatus = true;
766
+ annotation.invalidated = false;
767
+ }
768
+
769
+ return renderStatus;
770
+ };
771
+
772
+ private _updateAnnotation(
773
+ element: HTMLDivElement,
774
+ livewirePath: LivewirePath
775
+ ) {
776
+ if (!this.editData || !livewirePath) {
777
+ return;
778
+ }
779
+
780
+ const { pointArray: imagePoints } = livewirePath;
781
+ const worldPolylinePoints: Types.Point3[] = [];
782
+ const { sliceToWorld } = this.editData;
783
+
784
+ for (let i = 0, len = imagePoints.length; i < len; i++) {
785
+ const imagePoint = imagePoints[i];
786
+ const worldPoint = sliceToWorld(imagePoint);
787
+ worldPolylinePoints.push(worldPoint);
788
+ }
789
+
790
+ if (worldPolylinePoints.length > 1) {
791
+ worldPolylinePoints.push([...worldPolylinePoints[0]]);
792
+ }
793
+
794
+ this.editData.annotation.data.polyline = worldPolylinePoints;
795
+ }
796
+ }
797
+
798
+ LivewireContourTool.toolName = 'LivewireContour';
799
+ export default LivewireContourTool;