@cornerstonejs/tools 3.15.5 → 3.16.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 (38) hide show
  1. package/dist/esm/drawingSvg/drawFan.d.ts +4 -0
  2. package/dist/esm/drawingSvg/drawFan.js +62 -0
  3. package/dist/esm/drawingSvg/drawLine.js +2 -1
  4. package/dist/esm/drawingSvg/index.d.ts +2 -1
  5. package/dist/esm/drawingSvg/index.js +2 -1
  6. package/dist/esm/index.d.ts +2 -2
  7. package/dist/esm/index.js +2 -2
  8. package/dist/esm/tools/annotation/EllipticalROITool.js +17 -14
  9. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/UltrasoundPleuraBLineTool.d.ts +64 -0
  10. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/UltrasoundPleuraBLineTool.js +752 -0
  11. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/calculateFanShapeCorners.d.ts +8 -0
  12. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/calculateFanShapeCorners.js +143 -0
  13. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/deriveFanGeometry.d.ts +2 -0
  14. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/deriveFanGeometry.js +32 -0
  15. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/fanExtraction.d.ts +7 -0
  16. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/fanExtraction.js +171 -0
  17. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.d.ts +5 -0
  18. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.js +6 -0
  19. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/segmentLargestUSOutlineFromBuffer.d.ts +2 -0
  20. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/segmentLargestUSOutlineFromBuffer.js +123 -0
  21. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/types.d.ts +41 -0
  22. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/types.js +0 -0
  23. package/dist/esm/tools/index.d.ts +2 -1
  24. package/dist/esm/tools/index.js +2 -1
  25. package/dist/esm/types/ToolSpecificAnnotationTypes.d.ts +10 -0
  26. package/dist/esm/utilities/index.d.ts +2 -1
  27. package/dist/esm/utilities/index.js +2 -1
  28. package/dist/esm/utilities/math/fan/fanUtils.d.ts +10 -0
  29. package/dist/esm/utilities/math/fan/fanUtils.js +99 -0
  30. package/dist/esm/utilities/math/line/intersectLine.d.ts +1 -1
  31. package/dist/esm/utilities/math/line/intersectLine.js +16 -6
  32. package/dist/esm/utilities/math/polyline/convexHull.d.ts +2 -0
  33. package/dist/esm/utilities/math/polyline/convexHull.js +31 -0
  34. package/dist/esm/utilities/math/polyline/index.d.ts +2 -1
  35. package/dist/esm/utilities/math/polyline/index.js +2 -1
  36. package/dist/esm/version.d.ts +1 -1
  37. package/dist/esm/version.js +1 -1
  38. package/package.json +3 -3
@@ -0,0 +1,752 @@
1
+ import { Events, ChangeTypes } from '../../../enums';
2
+ import { getEnabledElement, utilities, metaData, getEnabledElementByViewportId, } from '@cornerstonejs/core';
3
+ import { AnnotationTool } from '../../base';
4
+ import { addAnnotation, getAnnotations, removeAnnotation, } from '../../../stateManagement/annotation/annotationState';
5
+ import { isAnnotationLocked } from '../../../stateManagement/annotation/annotationLocking';
6
+ import { isAnnotationVisible } from '../../../stateManagement/annotation/annotationVisibility';
7
+ import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../../stateManagement/annotation/helpers/state';
8
+ import * as lineSegment from '../../../utilities/math/line';
9
+ import { drawHandles as drawHandlesSvg, drawLine as drawLineSvg, drawFan as drawFanSvg, } from '../../../drawingSvg';
10
+ import { state } from '../../../store/state';
11
+ import { getViewportIdsWithToolToRender } from '../../../utilities/viewportFilters';
12
+ import triggerAnnotationRenderForViewportIds from '../../../utilities/triggerAnnotationRenderForViewportIds';
13
+ import { resetElementCursor, hideElementCursor, } from '../../../cursors/elementCursor';
14
+ import { angleFromCenter, calculateInnerFanPercentage, clipInterval, intervalFromPoints, mergeIntervals, subtractIntervals, } from '../../../utilities/math/fan/fanUtils';
15
+ import { calculateFanGeometry } from './utils/fanExtraction';
16
+ const { transformIndexToWorld } = utilities;
17
+ class UltrasoundPleuraBLineTool extends AnnotationTool {
18
+ static { this.toolName = 'UltrasoundPleuraBLineTool'; }
19
+ static { this.USPleuraBLineAnnotationType = {
20
+ BLINE: 'bLine',
21
+ PLEURA: 'pleura',
22
+ }; }
23
+ constructor(toolProps = {}, defaultToolProps = {
24
+ supportedInteractionTypes: ['Mouse', 'Touch'],
25
+ configuration: {
26
+ preventHandleOutsideImage: false,
27
+ getTextLines: defaultGetTextLines,
28
+ center: null,
29
+ innerRadius: null,
30
+ outerRadius: null,
31
+ startAngle: null,
32
+ endAngle: null,
33
+ bLineColor: 'rgb(60, 255, 60)',
34
+ pleuraColor: 'rgb(0, 4, 255)',
35
+ drawDepthGuide: true,
36
+ depth_ratio: 0.5,
37
+ depthGuideColor: 'rgb(0, 255, 255)',
38
+ depthGuideThickness: 4,
39
+ depthGuideDashLength: 20,
40
+ depthGuideDashGap: 16,
41
+ depthGuideOpacity: 0.2,
42
+ fanOpacity: 0.1,
43
+ showFanAnnotations: true,
44
+ updatePercentageCallback: null,
45
+ actions: {
46
+ undo: {
47
+ method: 'undo',
48
+ bindings: [{ key: 'z' }],
49
+ },
50
+ redo: {
51
+ method: 'redo',
52
+ bindings: [{ key: 'y' }],
53
+ },
54
+ },
55
+ },
56
+ }) {
57
+ super(toolProps, defaultToolProps);
58
+ this.pleuraAnnotations = [];
59
+ this.bLineAnnotations = [];
60
+ this.addNewAnnotation = (evt) => {
61
+ const eventDetail = evt.detail;
62
+ const { currentPoints, element } = eventDetail;
63
+ const worldPos = currentPoints.world;
64
+ const enabledElement = getEnabledElement(element);
65
+ const { viewport } = enabledElement;
66
+ hideElementCursor(element);
67
+ this.isDrawing = true;
68
+ const { viewPlaneNormal, viewUp, position: cameraPosition, } = viewport.getCamera();
69
+ const referencedImageId = this.getReferencedImageId(viewport, worldPos, viewPlaneNormal, viewUp);
70
+ const annotation = {
71
+ highlighted: true,
72
+ invalidated: true,
73
+ metadata: {
74
+ ...viewport.getViewReference({ points: [worldPos] }),
75
+ toolName: this.getToolName(),
76
+ referencedImageId,
77
+ viewUp,
78
+ cameraPosition,
79
+ },
80
+ data: {
81
+ handles: {
82
+ points: [[...worldPos], [...worldPos]],
83
+ activeHandleIndex: null,
84
+ },
85
+ annotationType: this.getActiveAnnotationType(),
86
+ label: '',
87
+ },
88
+ };
89
+ addAnnotation(annotation, element);
90
+ const viewportIdsToRender = getViewportIdsWithToolToRender(element, this.getToolName());
91
+ this.editData = {
92
+ annotation,
93
+ viewportIdsToRender,
94
+ handleIndex: 1,
95
+ movingTextBox: false,
96
+ newAnnotation: true,
97
+ hasMoved: false,
98
+ };
99
+ this._activateDraw(element);
100
+ evt.preventDefault();
101
+ triggerAnnotationRenderForViewportIds(viewportIdsToRender);
102
+ return annotation;
103
+ };
104
+ this.isPointNearTool = (element, annotation, canvasCoords, proximity) => {
105
+ const enabledElement = getEnabledElement(element);
106
+ const { viewport } = enabledElement;
107
+ const { data } = annotation;
108
+ const [point1, point2] = data.handles.points;
109
+ const canvasPoint1 = viewport.worldToCanvas(point1);
110
+ const canvasPoint2 = viewport.worldToCanvas(point2);
111
+ const line = {
112
+ start: {
113
+ x: canvasPoint1[0],
114
+ y: canvasPoint1[1],
115
+ },
116
+ end: {
117
+ x: canvasPoint2[0],
118
+ y: canvasPoint2[1],
119
+ },
120
+ };
121
+ const distanceToPoint = lineSegment.distanceToPoint([line.start.x, line.start.y], [line.end.x, line.end.y], [canvasCoords[0], canvasCoords[1]]);
122
+ if (distanceToPoint <= proximity) {
123
+ return true;
124
+ }
125
+ return false;
126
+ };
127
+ this.toolSelectedCallback = (evt, annotation) => {
128
+ const eventDetail = evt.detail;
129
+ const { element } = eventDetail;
130
+ annotation.highlighted = true;
131
+ const viewportIdsToRender = getViewportIdsWithToolToRender(element, this.getToolName());
132
+ this.editData = {
133
+ annotation,
134
+ viewportIdsToRender,
135
+ movingTextBox: false,
136
+ };
137
+ this._activateModify(element);
138
+ hideElementCursor(element);
139
+ triggerAnnotationRenderForViewportIds(viewportIdsToRender);
140
+ evt.preventDefault();
141
+ };
142
+ this._endCallback = (evt) => {
143
+ const eventDetail = evt.detail;
144
+ const { element } = eventDetail;
145
+ const { annotation, viewportIdsToRender, newAnnotation, hasMoved } = this.editData;
146
+ const { data } = annotation;
147
+ if (newAnnotation && !hasMoved) {
148
+ return;
149
+ }
150
+ data.handles.activeHandleIndex = null;
151
+ this._deactivateModify(element);
152
+ this._deactivateDraw(element);
153
+ resetElementCursor(element);
154
+ if (this.isHandleOutsideImage &&
155
+ this.configuration.preventHandleOutsideImage) {
156
+ removeAnnotation(annotation.annotationUID);
157
+ }
158
+ triggerAnnotationRenderForViewportIds(viewportIdsToRender);
159
+ this.doneEditMemo();
160
+ if (newAnnotation) {
161
+ triggerAnnotationCompleted(annotation);
162
+ }
163
+ this.editData = null;
164
+ this.isDrawing = false;
165
+ };
166
+ this._dragCallback = (evt) => {
167
+ this.isDrawing = true;
168
+ const eventDetail = evt.detail;
169
+ const { element } = eventDetail;
170
+ const { viewport } = getEnabledElement(element) || {};
171
+ if (!viewport) {
172
+ return;
173
+ }
174
+ const { annotation, viewportIdsToRender, handleIndex, movingTextBox, newAnnotation, } = this.editData;
175
+ const { data } = annotation;
176
+ this.createMemo(element, annotation, { newAnnotation });
177
+ if (movingTextBox) {
178
+ const { deltaPoints } = eventDetail;
179
+ const worldPosDelta = deltaPoints.world;
180
+ const { textBox } = data.handles;
181
+ const { worldPosition } = textBox;
182
+ worldPosition[0] += worldPosDelta[0];
183
+ worldPosition[1] += worldPosDelta[1];
184
+ worldPosition[2] += worldPosDelta[2];
185
+ textBox.hasMoved = true;
186
+ }
187
+ else if (handleIndex === undefined) {
188
+ const { deltaPoints } = eventDetail;
189
+ const worldPosDelta = deltaPoints.world;
190
+ const points = data.handles.points;
191
+ const allPointsInsideShape = points.every((point) => {
192
+ const newPoint = [
193
+ point[0] + worldPosDelta[0],
194
+ point[1] + worldPosDelta[1],
195
+ point[2] + worldPosDelta[2],
196
+ ];
197
+ return this.isInsideFanShape(viewport, newPoint);
198
+ });
199
+ if (allPointsInsideShape) {
200
+ points.forEach((point) => {
201
+ point[0] += worldPosDelta[0];
202
+ point[1] += worldPosDelta[1];
203
+ point[2] += worldPosDelta[2];
204
+ });
205
+ annotation.invalidated = true;
206
+ }
207
+ }
208
+ else {
209
+ const { currentPoints } = eventDetail;
210
+ const worldPos = currentPoints.world;
211
+ if (this.isInsideFanShape(viewport, worldPos)) {
212
+ data.handles.points[handleIndex] = [...worldPos];
213
+ annotation.invalidated = true;
214
+ }
215
+ }
216
+ this.editData.hasMoved = true;
217
+ triggerAnnotationRenderForViewportIds(viewportIdsToRender);
218
+ if (annotation.invalidated) {
219
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
220
+ }
221
+ };
222
+ this.cancel = (element) => {
223
+ if (this.isDrawing) {
224
+ this.isDrawing = false;
225
+ this._deactivateDraw(element);
226
+ this._deactivateModify(element);
227
+ resetElementCursor(element);
228
+ const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
229
+ const { data } = annotation;
230
+ annotation.highlighted = false;
231
+ data.handles.activeHandleIndex = null;
232
+ triggerAnnotationRenderForViewportIds(viewportIdsToRender);
233
+ if (newAnnotation) {
234
+ triggerAnnotationCompleted(annotation);
235
+ }
236
+ this.editData = null;
237
+ return annotation.annotationUID;
238
+ }
239
+ };
240
+ this._activateModify = (element) => {
241
+ state.isInteractingWithTool = true;
242
+ element.addEventListener(Events.MOUSE_UP, this._endCallback);
243
+ element.addEventListener(Events.MOUSE_DRAG, this._dragCallback);
244
+ element.addEventListener(Events.MOUSE_CLICK, this._endCallback);
245
+ element.addEventListener(Events.TOUCH_END, this._endCallback);
246
+ element.addEventListener(Events.TOUCH_DRAG, this._dragCallback);
247
+ element.addEventListener(Events.TOUCH_TAP, this._endCallback);
248
+ };
249
+ this._deactivateModify = (element) => {
250
+ state.isInteractingWithTool = false;
251
+ element.removeEventListener(Events.MOUSE_UP, this._endCallback);
252
+ element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback);
253
+ element.removeEventListener(Events.MOUSE_CLICK, this._endCallback);
254
+ element.removeEventListener(Events.TOUCH_END, this._endCallback);
255
+ element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback);
256
+ element.removeEventListener(Events.TOUCH_TAP, this._endCallback);
257
+ };
258
+ this._activateDraw = (element) => {
259
+ state.isInteractingWithTool = true;
260
+ element.addEventListener(Events.MOUSE_UP, this._endCallback);
261
+ element.addEventListener(Events.MOUSE_DRAG, this._dragCallback);
262
+ element.addEventListener(Events.MOUSE_MOVE, this._dragCallback);
263
+ element.addEventListener(Events.MOUSE_CLICK, this._endCallback);
264
+ element.addEventListener(Events.TOUCH_END, this._endCallback);
265
+ element.addEventListener(Events.TOUCH_DRAG, this._dragCallback);
266
+ element.addEventListener(Events.TOUCH_TAP, this._endCallback);
267
+ };
268
+ this._deactivateDraw = (element) => {
269
+ state.isInteractingWithTool = false;
270
+ element.removeEventListener(Events.MOUSE_UP, this._endCallback);
271
+ element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback);
272
+ element.removeEventListener(Events.MOUSE_MOVE, this._dragCallback);
273
+ element.removeEventListener(Events.MOUSE_CLICK, this._endCallback);
274
+ element.removeEventListener(Events.TOUCH_END, this._endCallback);
275
+ element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback);
276
+ element.removeEventListener(Events.TOUCH_TAP, this._endCallback);
277
+ };
278
+ this.renderAnnotation = (enabledElement, svgDrawingHelper) => {
279
+ let renderStatus = false;
280
+ const { viewport } = enabledElement;
281
+ const { element } = viewport;
282
+ if (!this.getFanShapeGeometryParameters(viewport)) {
283
+ return;
284
+ }
285
+ const { imageData } = viewport.getImageData() || {};
286
+ if (!imageData) {
287
+ return renderStatus;
288
+ }
289
+ if (this.configuration.drawDepthGuide) {
290
+ this.drawDepthGuide(svgDrawingHelper, viewport);
291
+ }
292
+ let annotations = getAnnotations(this.getToolName(), element);
293
+ if (!annotations?.length) {
294
+ return renderStatus;
295
+ }
296
+ annotations = this.filterInteractableAnnotationsForElement(element, annotations);
297
+ if (!annotations?.length) {
298
+ return renderStatus;
299
+ }
300
+ const targetId = this.getTargetId(viewport);
301
+ const renderingEngine = viewport.getRenderingEngine();
302
+ const styleSpecifier = {
303
+ toolGroupId: this.toolGroupId,
304
+ toolName: this.getToolName(),
305
+ viewportId: enabledElement.viewport.id,
306
+ };
307
+ const fanCenter = viewport.worldToCanvas(transformIndexToWorld(imageData, this.configuration.center));
308
+ const indexToCanvasRatio = this.getIndexToCanvasRatio(viewport);
309
+ const innerRadius = this.configuration.innerRadius * indexToCanvasRatio;
310
+ const outerRadius = this.configuration.outerRadius * indexToCanvasRatio;
311
+ const currentImageId = viewport.getCurrentImageId();
312
+ const unMergedPleuraIntervals = annotations
313
+ .filter((annotation) => annotation.data.annotationType ===
314
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA &&
315
+ annotation.metadata.referencedImageId === currentImageId)
316
+ .map((annotation) => {
317
+ const canvasCoordinates = annotation.data.handles.points.map((p) => viewport.worldToCanvas(p));
318
+ const interval = intervalFromPoints(fanCenter, canvasCoordinates);
319
+ return interval;
320
+ });
321
+ const mergedPleuraIntervals = mergeIntervals(unMergedPleuraIntervals);
322
+ const pleuraIntervalsDisplayed = [];
323
+ const bLineIntervalsDisplayed = [];
324
+ const drawAnnotation = (annotation) => {
325
+ const { annotationUID, data } = annotation;
326
+ const { points, activeHandleIndex } = data.handles;
327
+ styleSpecifier.annotationUID = annotationUID;
328
+ const { color, lineWidth, lineDash, shadow } = this.getAnnotationStyle({
329
+ annotation,
330
+ styleSpecifier,
331
+ });
332
+ const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
333
+ if (!viewport.getRenderingEngine()) {
334
+ console.warn('Rendering Engine has been destroyed');
335
+ return renderStatus;
336
+ }
337
+ let activeHandleCanvasCoords;
338
+ if (!isAnnotationVisible(annotationUID)) {
339
+ return;
340
+ }
341
+ if (!isAnnotationLocked(annotationUID) &&
342
+ !this.editData &&
343
+ activeHandleIndex !== null) {
344
+ activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]];
345
+ }
346
+ if (activeHandleCanvasCoords) {
347
+ const handleGroupUID = '0';
348
+ drawHandlesSvg(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, {
349
+ color: this.getColorForLineType(annotation),
350
+ fill: this.getColorForLineType(annotation),
351
+ lineDash,
352
+ lineWidth,
353
+ });
354
+ }
355
+ const dataId = `${annotationUID}-line`;
356
+ const lineUID = '1';
357
+ drawLineSvg(svgDrawingHelper, annotationUID, lineUID, canvasCoordinates[0], canvasCoordinates[1], {
358
+ color: this.getColorForLineType(annotation),
359
+ width: lineWidth,
360
+ lineDash,
361
+ shadow,
362
+ }, dataId);
363
+ if (this.configuration.showFanAnnotations) {
364
+ const lineInterval = intervalFromPoints(fanCenter, canvasCoordinates);
365
+ let fanNumber = 0;
366
+ if (annotation.data.annotationType ===
367
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE) {
368
+ const uncoveredIntervals = subtractIntervals(bLineIntervalsDisplayed, lineInterval);
369
+ uncoveredIntervals.forEach((interval) => {
370
+ const clippedIntervals = clipInterval(interval, mergedPleuraIntervals);
371
+ clippedIntervals.forEach((clippedInterval) => {
372
+ fanNumber++;
373
+ const fanIndex = fanNumber;
374
+ const fanDataId = `${annotationUID}-fan-${fanIndex}`;
375
+ const fanUID = `2-${fanIndex}`;
376
+ drawFanSvg(svgDrawingHelper, annotationUID, fanUID, fanCenter, innerRadius, outerRadius, clippedInterval[0], clippedInterval[1], {
377
+ color: 'transparent',
378
+ fill: this.getColorForLineType(annotation),
379
+ fillOpacity: this.configuration.fanOpacity,
380
+ width: lineWidth,
381
+ lineDash,
382
+ shadow,
383
+ }, fanDataId, 10);
384
+ bLineIntervalsDisplayed.push(clippedInterval);
385
+ });
386
+ });
387
+ }
388
+ else if (annotation.data.annotationType ===
389
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA) {
390
+ const uncoveredIntervals = subtractIntervals(pleuraIntervalsDisplayed, lineInterval);
391
+ uncoveredIntervals.forEach((interval, index) => {
392
+ fanNumber++;
393
+ const fanIndex = fanNumber;
394
+ const fanDataId = `${annotationUID}-fan-${fanIndex}`;
395
+ const fanUID = `2-${fanIndex}`;
396
+ drawFanSvg(svgDrawingHelper, annotationUID, fanUID, fanCenter, innerRadius, outerRadius, interval[0], interval[1], {
397
+ color: 'transparent',
398
+ fill: this.getColorForLineType(annotation),
399
+ fillOpacity: this.configuration.fanOpacity,
400
+ width: lineWidth,
401
+ lineDash,
402
+ shadow,
403
+ }, fanDataId, 5);
404
+ pleuraIntervalsDisplayed.push(interval);
405
+ });
406
+ }
407
+ }
408
+ };
409
+ const pleuraAnnotationsToDraw = annotations.filter((annotation) => annotation.data.annotationType ===
410
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA &&
411
+ annotation.metadata.referencedImageId === currentImageId);
412
+ pleuraAnnotationsToDraw.forEach((annotation) => {
413
+ if (!viewport.getRenderingEngine()) {
414
+ console.warn('Rendering Engine has been destroyed');
415
+ return renderStatus;
416
+ }
417
+ drawAnnotation(annotation);
418
+ });
419
+ const bLineAnnotationsToDraw = annotations.filter((annotation) => annotation.data.annotationType ===
420
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE &&
421
+ annotation.metadata.referencedImageId === currentImageId);
422
+ bLineAnnotationsToDraw.forEach((annotation) => {
423
+ if (!viewport.getRenderingEngine()) {
424
+ console.warn('Rendering Engine has been destroyed');
425
+ return renderStatus;
426
+ }
427
+ drawAnnotation(annotation);
428
+ });
429
+ renderStatus = true;
430
+ if (this.configuration.updatePercentageCallback && viewport) {
431
+ this.configuration.updatePercentageCallback(this.calculateBLinePleuraPercentage(viewport));
432
+ }
433
+ return renderStatus;
434
+ };
435
+ this.activeAnnotationType =
436
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE;
437
+ }
438
+ static filterAnnotations(element, filterFunction = () => true) {
439
+ const annotations = getAnnotations(UltrasoundPleuraBLineTool.toolName, element);
440
+ if (!annotations?.length) {
441
+ return [];
442
+ }
443
+ const filteredAnnotations = annotations.filter((annotation) => {
444
+ const currentImageId = annotation.metadata.referencedImageId;
445
+ return filterFunction(currentImageId);
446
+ });
447
+ return filteredAnnotations;
448
+ }
449
+ static countAnnotations(element, filterFunction = () => true) {
450
+ const annotations = getAnnotations(UltrasoundPleuraBLineTool.toolName, element);
451
+ const { viewport } = getEnabledElement(element);
452
+ const imageIds = viewport.getImageIds();
453
+ const getImageIdIndex = (imageId) => {
454
+ const index = imageIds.findIndex((id) => id === imageId);
455
+ if (index === -1) {
456
+ return 0;
457
+ }
458
+ return index;
459
+ };
460
+ if (!annotations?.length) {
461
+ return;
462
+ }
463
+ const annotationMapping = new Map();
464
+ annotations.forEach((annotation) => {
465
+ const currentImageId = annotation.metadata.referencedImageId;
466
+ if (!filterFunction(currentImageId)) {
467
+ return;
468
+ }
469
+ const { annotationType } = annotation.data;
470
+ let counts;
471
+ if (annotationMapping.has(currentImageId)) {
472
+ counts = annotationMapping.get(currentImageId);
473
+ }
474
+ else {
475
+ counts = {
476
+ frame: getImageIdIndex(currentImageId),
477
+ bLine: 0,
478
+ pleura: 0,
479
+ };
480
+ }
481
+ if (annotationType ===
482
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA) {
483
+ counts.pleura++;
484
+ }
485
+ else if (annotationType ===
486
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE) {
487
+ counts.bLine++;
488
+ }
489
+ annotationMapping.set(currentImageId, counts);
490
+ });
491
+ return annotationMapping;
492
+ }
493
+ static deleteAnnotations(element, filterFunction = () => false) {
494
+ const annotations = getAnnotations(UltrasoundPleuraBLineTool.toolName, element);
495
+ if (!annotations?.length) {
496
+ return;
497
+ }
498
+ annotations.forEach((annotation) => {
499
+ if (!filterFunction(annotation.metadata.referencedImageId)) {
500
+ return;
501
+ }
502
+ removeAnnotation(annotation.annotationUID);
503
+ });
504
+ }
505
+ setActiveAnnotationType(type) {
506
+ this.activeAnnotationType = type;
507
+ }
508
+ getActiveAnnotationType() {
509
+ return this.activeAnnotationType;
510
+ }
511
+ deleteLastAnnotationType(element, type) {
512
+ let annotationList;
513
+ const annotations = getAnnotations(UltrasoundPleuraBLineTool.toolName, element);
514
+ if (type === UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA) {
515
+ annotationList = annotations.filter((annotation) => annotation.data.annotationType ===
516
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA);
517
+ }
518
+ else if (type === UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE) {
519
+ annotationList = annotations.filter((annotation) => annotation.data.annotationType ===
520
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE);
521
+ }
522
+ if (annotationList?.length > 0) {
523
+ const annotation = annotationList.pop();
524
+ removeAnnotation(annotation.annotationUID);
525
+ }
526
+ }
527
+ static { this.hydrate = (viewportId, points, options) => {
528
+ const enabledElement = getEnabledElementByViewportId(viewportId);
529
+ if (!enabledElement) {
530
+ return;
531
+ }
532
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(UltrasoundPleuraBLineTool, enabledElement, points, options);
533
+ const { toolInstance, ...serializableOptions } = options || {};
534
+ const annotation = {
535
+ annotationUID: options?.annotationUID || utilities.uuidv4(),
536
+ data: {
537
+ handles: {
538
+ points,
539
+ },
540
+ },
541
+ highlighted: false,
542
+ autoGenerated: false,
543
+ invalidated: false,
544
+ isLocked: false,
545
+ isVisible: true,
546
+ metadata: {
547
+ toolName: instance.getToolName(),
548
+ viewPlaneNormal,
549
+ FrameOfReferenceUID,
550
+ referencedImageId,
551
+ ...serializableOptions,
552
+ },
553
+ };
554
+ addAnnotation(annotation, viewport.element);
555
+ triggerAnnotationRenderForViewportIds([viewport.id]);
556
+ }; }
557
+ handleSelectedCallback(evt, annotation, handle) {
558
+ const eventDetail = evt.detail;
559
+ const { element } = eventDetail;
560
+ const { data } = annotation;
561
+ annotation.highlighted = true;
562
+ let movingTextBox = false;
563
+ let handleIndex;
564
+ if (handle.worldPosition) {
565
+ movingTextBox = true;
566
+ }
567
+ else {
568
+ handleIndex = data.handles.points.findIndex((p) => p === handle);
569
+ }
570
+ const viewportIdsToRender = getViewportIdsWithToolToRender(element, this.getToolName());
571
+ this.editData = {
572
+ annotation,
573
+ viewportIdsToRender,
574
+ handleIndex,
575
+ movingTextBox,
576
+ };
577
+ this._activateModify(element);
578
+ hideElementCursor(element);
579
+ triggerAnnotationRenderForViewportIds(viewportIdsToRender);
580
+ evt.preventDefault();
581
+ }
582
+ isInsideFanShape(viewport, point) {
583
+ if (!this.getFanShapeGeometryParameters(viewport)) {
584
+ return false;
585
+ }
586
+ const { imageData } = viewport.getImageData() || {};
587
+ if (imageData) {
588
+ const fanCenter = viewport.worldToCanvas(imageData.indexToWorld(this.configuration.center));
589
+ const canvasCoordinates = viewport.worldToCanvas(point);
590
+ const angle = angleFromCenter(fanCenter, canvasCoordinates);
591
+ return (angle >= this.configuration.startAngle &&
592
+ angle <= this.configuration.endAngle);
593
+ }
594
+ }
595
+ updateFanGeometryConfiguration(fanGeometry) {
596
+ if (!fanGeometry) {
597
+ return;
598
+ }
599
+ if (this.isFanShapeGeometryParametersValid(fanGeometry)) {
600
+ this.configuration.center = [
601
+ fanGeometry.center[0],
602
+ fanGeometry.center[1],
603
+ 0,
604
+ ];
605
+ }
606
+ this.configuration.innerRadius = fanGeometry.innerRadius;
607
+ this.configuration.outerRadius = fanGeometry.outerRadius;
608
+ this.configuration.startAngle = fanGeometry.startAngle;
609
+ this.configuration.endAngle = fanGeometry.endAngle;
610
+ }
611
+ deriveFanGeometryFromViewport(viewport) {
612
+ const imageId = viewport.getCurrentImageId();
613
+ const { fanGeometry } = calculateFanGeometry(imageId) || {};
614
+ if (fanGeometry) {
615
+ this.updateFanGeometryConfiguration(fanGeometry);
616
+ }
617
+ }
618
+ isFanShapeGeometryParametersValid(fanGeometry) {
619
+ if (!fanGeometry) {
620
+ fanGeometry = this.configuration;
621
+ }
622
+ return (fanGeometry?.center &&
623
+ fanGeometry?.innerRadius > 0 &&
624
+ fanGeometry?.outerRadius &&
625
+ fanGeometry?.startAngle > 0 &&
626
+ fanGeometry?.startAngle < 360 &&
627
+ fanGeometry?.endAngle > 0 &&
628
+ fanGeometry?.endAngle < 360);
629
+ }
630
+ getFanShapeGeometryParameters(viewport) {
631
+ if (this.isFanShapeGeometryParametersValid()) {
632
+ return true;
633
+ }
634
+ if (!this.isFanShapeGeometryParametersValid()) {
635
+ const imageId = viewport.getCurrentImageId();
636
+ const fanGeometry = metaData.get('ultrasoundFanShapeGeometry', imageId);
637
+ this.updateFanGeometryConfiguration(fanGeometry);
638
+ }
639
+ if (!this.isFanShapeGeometryParametersValid()) {
640
+ this.deriveFanGeometryFromViewport(viewport);
641
+ }
642
+ return this.isFanShapeGeometryParametersValid();
643
+ }
644
+ calculateBLinePleuraPercentage(viewport) {
645
+ if (!this.getFanShapeGeometryParameters(viewport)) {
646
+ return;
647
+ }
648
+ const { imageData } = viewport.getImageData() || {};
649
+ if (!imageData) {
650
+ return;
651
+ }
652
+ const { element } = viewport;
653
+ const fanCenter = viewport.worldToCanvas(imageData.indexToWorld(this.configuration.center));
654
+ const currentImageId = viewport.getCurrentImageId();
655
+ const annotations = getAnnotations(this.getToolName(), element) || [];
656
+ const pleuraIntervals = annotations
657
+ .filter((annotation) => annotation.data.annotationType ===
658
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA &&
659
+ annotation.metadata.referencedImageId === currentImageId)
660
+ .map((annotation) => {
661
+ const canvasCoordinates = annotation.data.handles.points.map((p) => viewport.worldToCanvas(p));
662
+ return canvasCoordinates;
663
+ });
664
+ const bLineIntervals = annotations
665
+ .filter((annotation) => annotation.data.annotationType ===
666
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE &&
667
+ annotation.metadata.referencedImageId === currentImageId)
668
+ .map((annotation) => {
669
+ const canvasCoordinates = annotation.data.handles.points.map((p) => viewport.worldToCanvas(p));
670
+ return canvasCoordinates;
671
+ });
672
+ return calculateInnerFanPercentage(fanCenter, pleuraIntervals, bLineIntervals);
673
+ }
674
+ getColorForLineType(annotation) {
675
+ const { annotationType } = annotation.data;
676
+ const { bLineColor, pleuraColor } = this.configuration;
677
+ if (annotationType ===
678
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.BLINE) {
679
+ return bLineColor;
680
+ }
681
+ if (annotationType ===
682
+ UltrasoundPleuraBLineTool.USPleuraBLineAnnotationType.PLEURA) {
683
+ return pleuraColor;
684
+ }
685
+ return bLineColor;
686
+ }
687
+ getIndexToCanvasRatio(viewport) {
688
+ const { imageData } = viewport.getImageData() || {};
689
+ const v1 = viewport.worldToCanvas(imageData.indexToWorld([1, 0, 0]));
690
+ const v2 = viewport.worldToCanvas(imageData.indexToWorld([2, 0, 0]));
691
+ const diffVector = [v2[0] - v1[0], v2[1] - v1[1]];
692
+ const vectorSize = Math.sqrt(diffVector[0] * diffVector[0] + diffVector[1] * diffVector[1]);
693
+ return vectorSize;
694
+ }
695
+ drawDepthGuide(svgDrawingHelper, viewport) {
696
+ if (!this.getFanShapeGeometryParameters(viewport)) {
697
+ return;
698
+ }
699
+ const { imageData } = viewport.getImageData() || {};
700
+ if (!imageData) {
701
+ return;
702
+ }
703
+ const radToDegree = (rad) => (rad * 180) / Math.PI;
704
+ const degreeToRad = (degree) => (degree * Math.PI) / 180;
705
+ const indexToCanvas = (point) => {
706
+ return viewport.worldToCanvas(transformIndexToWorld(imageData, point));
707
+ };
708
+ const depth_radius = this.configuration.innerRadius +
709
+ this.configuration.depth_ratio *
710
+ (this.configuration.outerRadius - this.configuration.innerRadius);
711
+ const theta_start = this.configuration.startAngle;
712
+ const theta_end = this.configuration.endAngle;
713
+ const theta_range = theta_end - theta_start;
714
+ const arc_length = degreeToRad(theta_range) * depth_radius;
715
+ let num_dashes = Math.round(arc_length /
716
+ (this.configuration.depthGuideDashLength +
717
+ this.configuration.depthGuideDashGap));
718
+ if (num_dashes <= 0) {
719
+ num_dashes = Math.max(15, Math.round(theta_range / 5));
720
+ }
721
+ const theta_step = theta_range / num_dashes;
722
+ for (let i = 0; i < num_dashes; i++) {
723
+ const theta1 = degreeToRad(theta_start + i * theta_step);
724
+ const theta2 = degreeToRad(theta_start +
725
+ i * theta_step +
726
+ radToDegree(this.configuration.depthGuideDashLength) / depth_radius);
727
+ const start_point = [
728
+ this.configuration.center[0] + depth_radius * Math.cos(theta1),
729
+ this.configuration.center[1] + depth_radius * Math.sin(theta1),
730
+ 0,
731
+ ];
732
+ const end_point = [
733
+ this.configuration.center[0] + depth_radius * Math.cos(theta2),
734
+ this.configuration.center[1] + depth_radius * Math.sin(theta2),
735
+ 0,
736
+ ];
737
+ drawLineSvg(svgDrawingHelper, viewport.id, `depthGuide-${i}`, indexToCanvas(start_point), indexToCanvas(end_point), {
738
+ color: this.configuration.depthGuideColor,
739
+ lineWidth: this.configuration.depthGuideThickness,
740
+ strokeOpacity: this.configuration.depthGuideOpacity,
741
+ });
742
+ }
743
+ }
744
+ _isInsideVolume(index1, index2, dimensions) {
745
+ return (utilities.indexWithinDimensions(index1, dimensions) &&
746
+ utilities.indexWithinDimensions(index2, dimensions));
747
+ }
748
+ }
749
+ function defaultGetTextLines(data, targetId) {
750
+ return [''];
751
+ }
752
+ export default UltrasoundPleuraBLineTool;