@cornerstonejs/tools 1.78.3 → 1.80.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 (91) hide show
  1. package/dist/cjs/cursors/SVGCursorDescriptor.js +7 -0
  2. package/dist/cjs/cursors/SVGCursorDescriptor.js.map +1 -1
  3. package/dist/cjs/drawingSvg/drawHeight.d.ts +3 -0
  4. package/dist/cjs/drawingSvg/drawHeight.js +49 -0
  5. package/dist/cjs/drawingSvg/drawHeight.js.map +1 -0
  6. package/dist/cjs/drawingSvg/index.d.ts +2 -1
  7. package/dist/cjs/drawingSvg/index.js +3 -1
  8. package/dist/cjs/drawingSvg/index.js.map +1 -1
  9. package/dist/cjs/index.d.ts +2 -2
  10. package/dist/cjs/index.js +3 -2
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/tools/annotation/HeightTool.d.ts +40 -0
  13. package/dist/cjs/tools/annotation/HeightTool.js +463 -0
  14. package/dist/cjs/tools/annotation/HeightTool.js.map +1 -0
  15. package/dist/cjs/tools/index.d.ts +2 -1
  16. package/dist/cjs/tools/index.js +4 -2
  17. package/dist/cjs/tools/index.js.map +1 -1
  18. package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +15 -8
  19. package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js +189 -62
  20. package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -1
  21. package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +17 -7
  22. package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js +167 -52
  23. package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
  24. package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +26 -4
  25. package/dist/cjs/utilities/planar/filterAnnotationsWithinPlane.d.ts +3 -0
  26. package/dist/cjs/utilities/planar/filterAnnotationsWithinPlane.js +31 -0
  27. package/dist/cjs/utilities/planar/filterAnnotationsWithinPlane.js.map +1 -0
  28. package/dist/cjs/utilities/planar/index.d.ts +3 -1
  29. package/dist/cjs/utilities/planar/index.js +4 -1
  30. package/dist/cjs/utilities/planar/index.js.map +1 -1
  31. package/dist/cjs/utilities/viewport/isViewportPreScaled.js +1 -4
  32. package/dist/cjs/utilities/viewport/isViewportPreScaled.js.map +1 -1
  33. package/dist/esm/cursors/SVGCursorDescriptor.js +7 -0
  34. package/dist/esm/cursors/SVGCursorDescriptor.js.map +1 -1
  35. package/dist/esm/drawingSvg/drawHeight.js +43 -0
  36. package/dist/esm/drawingSvg/drawHeight.js.map +1 -0
  37. package/dist/esm/drawingSvg/index.js +2 -1
  38. package/dist/esm/drawingSvg/index.js.map +1 -1
  39. package/dist/esm/index.js +2 -2
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/tools/annotation/HeightTool.js +439 -0
  42. package/dist/esm/tools/annotation/HeightTool.js.map +1 -0
  43. package/dist/esm/tools/index.js +2 -1
  44. package/dist/esm/tools/index.js.map +1 -1
  45. package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +192 -66
  46. package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -1
  47. package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js +168 -54
  48. package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
  49. package/dist/esm/utilities/planar/filterAnnotationsWithinPlane.js +27 -0
  50. package/dist/esm/utilities/planar/filterAnnotationsWithinPlane.js.map +1 -0
  51. package/dist/esm/utilities/planar/index.js +3 -1
  52. package/dist/esm/utilities/planar/index.js.map +1 -1
  53. package/dist/esm/utilities/viewport/isViewportPreScaled.js +2 -5
  54. package/dist/esm/utilities/viewport/isViewportPreScaled.js.map +1 -1
  55. package/dist/types/cursors/SVGCursorDescriptor.d.ts.map +1 -1
  56. package/dist/types/drawingSvg/drawHeight.d.ts +4 -0
  57. package/dist/types/drawingSvg/drawHeight.d.ts.map +1 -0
  58. package/dist/types/drawingSvg/index.d.ts +2 -1
  59. package/dist/types/drawingSvg/index.d.ts.map +1 -1
  60. package/dist/types/index.d.ts +2 -2
  61. package/dist/types/index.d.ts.map +1 -1
  62. package/dist/types/tools/annotation/HeightTool.d.ts +41 -0
  63. package/dist/types/tools/annotation/HeightTool.d.ts.map +1 -0
  64. package/dist/types/tools/index.d.ts +2 -1
  65. package/dist/types/tools/index.d.ts.map +1 -1
  66. package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +15 -8
  67. package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts.map +1 -1
  68. package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +17 -7
  69. package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts.map +1 -1
  70. package/dist/types/types/ToolSpecificAnnotationTypes.d.ts +26 -4
  71. package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
  72. package/dist/types/utilities/planar/filterAnnotationsWithinPlane.d.ts +4 -0
  73. package/dist/types/utilities/planar/filterAnnotationsWithinPlane.d.ts.map +1 -0
  74. package/dist/types/utilities/planar/index.d.ts +3 -1
  75. package/dist/types/utilities/planar/index.d.ts.map +1 -1
  76. package/dist/types/utilities/viewport/isViewportPreScaled.d.ts.map +1 -1
  77. package/dist/umd/index.js +1 -1
  78. package/dist/umd/index.js.map +1 -1
  79. package/package.json +3 -3
  80. package/src/cursors/SVGCursorDescriptor.ts +7 -0
  81. package/src/drawingSvg/drawHeight.ts +90 -0
  82. package/src/drawingSvg/index.ts +2 -0
  83. package/src/index.ts +2 -0
  84. package/src/tools/annotation/HeightTool.ts +882 -0
  85. package/src/tools/index.ts +2 -0
  86. package/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +310 -102
  87. package/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +287 -77
  88. package/src/types/ToolSpecificAnnotationTypes.ts +26 -4
  89. package/src/utilities/planar/filterAnnotationsWithinPlane.ts +76 -0
  90. package/src/utilities/planar/index.ts +3 -0
  91. package/src/utilities/viewport/isViewportPreScaled.ts +2 -5
@@ -4,13 +4,10 @@ import {
4
4
  cache,
5
5
  getEnabledElement,
6
6
  utilities as csUtils,
7
- metaData,
8
- triggerEvent,
9
- eventTarget,
7
+ utilities as coreUtils,
10
8
  } from '@cornerstonejs/core';
11
9
 
12
10
  import { vec3 } from 'gl-matrix';
13
- import { Events } from '../../enums';
14
11
  import {
15
12
  addAnnotation,
16
13
  removeAnnotation,
@@ -20,17 +17,22 @@ import { isAnnotationLocked } from '../../stateManagement/annotation/annotationL
20
17
  import {
21
18
  drawCircle as drawCircleSvg,
22
19
  drawHandles as drawHandlesSvg,
20
+ drawLinkedTextBox as drawLinkedTextBoxSvg,
23
21
  } from '../../drawingSvg';
24
22
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
23
+ import getWorldWidthAndHeightFromTwoPoints from '../../utilities/planar/getWorldWidthAndHeightFromTwoPoints';
24
+ import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
25
25
  import throttle from '../../utilities/throttle';
26
- import { AnnotationModifiedEventDetail } from '../../types/EventTypes';
27
26
  import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
28
27
  import {
29
28
  hideElementCursor,
30
29
  resetElementCursor,
31
30
  } from '../../cursors/elementCursor';
32
31
  import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
33
- import { triggerAnnotationCompleted } from '../../stateManagement/annotation/helpers/state';
32
+ import {
33
+ triggerAnnotationCompleted,
34
+ triggerAnnotationModified,
35
+ } from '../../stateManagement/annotation/helpers/state';
34
36
  import {
35
37
  PublicToolProps,
36
38
  ToolProps,
@@ -44,8 +46,18 @@ import {
44
46
  getCanvasCircleCorners,
45
47
  getCanvasCircleRadius,
46
48
  } from '../../utilities/math/circle';
49
+ import {
50
+ getCalibratedLengthUnitsAndScale,
51
+ getCalibratedAspect,
52
+ } from '../../utilities/getCalibratedUnits';
53
+ import { getModalityUnit } from '../../utilities/getModalityUnit';
54
+ import { isViewportPreScaled } from '../../utilities/viewport/isViewportPreScaled';
47
55
  import { pointInEllipse } from '../../utilities/math/ellipse';
48
- import { pointInShapeCallback } from '../../utilities';
56
+ import { pointInShapeCallback, roundNumber } from '../../utilities';
57
+ import { BasicStatsCalculator } from '../../utilities/math/basic';
58
+
59
+ import cloneDeep from 'lodash.clonedeep';
60
+ import { filterAnnotationsWithinSamePlane } from '../../utilities/planar';
49
61
 
50
62
  const { transformWorldToIndex } = csUtils;
51
63
 
@@ -72,6 +84,9 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
72
84
  configuration: {
73
85
  numSlicesToPropagate: 10,
74
86
  calculatePointsInsideVolume: false,
87
+ getTextLines: defaultGetTextLines,
88
+ statsCalculator: BasicStatsCalculator,
89
+ showTextBox: false,
75
90
  },
76
91
  }
77
92
  ) {
@@ -120,28 +135,22 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
120
135
  );
121
136
  }
122
137
 
123
- // if (!referencedImageId) {
124
- // throw new Error('This tool does not work on non-acquisition planes');
125
- // }
126
-
127
138
  const spacingInNormal = csUtils.getSpacingInNormalDirection(
128
139
  imageVolume,
129
140
  viewPlaneNormal
130
141
  );
131
142
 
132
- const newStartIndex = this._getStartSliceIndex(
133
- imageVolume,
143
+ const startCoord = this._getStartCoordinate(
134
144
  worldPos,
135
145
  spacingInNormal,
136
146
  viewPlaneNormal
137
147
  );
138
148
 
139
- // We cannot newStartIndex add numSlicesToPropagate to startIndex because
149
+ // We cannot simply add numSlicesToPropagate to startIndex because
140
150
  // the order of imageIds can be from top to bottom or bottom to top and
141
151
  // we want to make sure it is always propagated in the direction of the
142
152
  // view and also to make sure we don't go out of bounds.
143
- const endIndex = this._getEndSliceIndex(
144
- imageVolume,
153
+ const endCoord = this._getEndCoordinate(
145
154
  worldPos,
146
155
  spacingInNormal,
147
156
  viewPlaneNormal
@@ -164,14 +173,19 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
164
173
  },
165
174
  data: {
166
175
  label: '',
167
- startSlice: newStartIndex,
168
- endSlice: endIndex,
176
+ startCoordinate: startCoord,
177
+ endCoordinate: endCoord,
169
178
 
170
179
  handles: {
171
180
  textBox: {
172
181
  hasMoved: false,
173
- worldPosition: null,
174
- worldBoundingBox: null,
182
+ worldPosition: <Types.Point3>[0, 0, 0],
183
+ worldBoundingBox: {
184
+ topLeft: <Types.Point3>[0, 0, 0],
185
+ topRight: <Types.Point3>[0, 0, 0],
186
+ bottomLeft: <Types.Point3>[0, 0, 0],
187
+ bottomRight: <Types.Point3>[0, 0, 0],
188
+ },
175
189
  },
176
190
  points: [[...worldPos], [...worldPos]] as [
177
191
  Types.Point3, // center
@@ -182,11 +196,17 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
182
196
  cachedStats: {
183
197
  pointsInVolume: [],
184
198
  projectionPoints: [],
199
+ statistics: [],
185
200
  },
186
201
  labelmapUID: null,
187
202
  },
188
203
  };
189
204
 
205
+ // update the projection points in 3D space, since we are projecting
206
+ // the points to the slice plane, we need to make sure the points are
207
+ // computed for later export
208
+ this._computeProjectionPoints(annotation, imageVolume);
209
+
190
210
  addAnnotation(annotation, element);
191
211
 
192
212
  const viewportIdsToRender = getViewportIdsWithToolToRender(
@@ -251,7 +271,12 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
251
271
  const imageVolume = cache.getVolume(targetId.split(/volumeId:|\?/)[1]);
252
272
 
253
273
  if (this.configuration.calculatePointsInsideVolume) {
254
- this._computePointsInsideVolume(annotation, imageVolume, enabledElement);
274
+ this._computePointsInsideVolume(
275
+ annotation,
276
+ imageVolume,
277
+ targetId,
278
+ enabledElement
279
+ );
255
280
  }
256
281
 
257
282
  triggerAnnotationRenderForViewportIds(
@@ -278,14 +303,16 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
278
303
  ): boolean => {
279
304
  let renderStatus = false;
280
305
  const { viewport } = enabledElement;
281
-
282
- const annotations = getAnnotations(this.getToolName(), viewport.element);
306
+ let annotations = getAnnotations(this.getToolName(), viewport.element);
283
307
 
284
308
  if (!annotations?.length) {
285
309
  return renderStatus;
286
310
  }
287
311
 
288
- const sliceIndex = viewport.getCurrentImageIdIndex();
312
+ annotations = filterAnnotationsWithinSamePlane(
313
+ annotations,
314
+ viewport.getCamera()
315
+ );
289
316
 
290
317
  const styleSpecifier: StyleSpecifier = {
291
318
  toolGroupId: this.toolGroupId,
@@ -296,7 +323,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
296
323
  for (let i = 0; i < annotations.length; i++) {
297
324
  const annotation = annotations[i] as CircleROIStartEndThresholdAnnotation;
298
325
  const { annotationUID, data } = annotation;
299
- const { startSlice, endSlice } = data;
326
+ const { startCoordinate, endCoordinate } = data;
300
327
  const { points, activeHandleIndex } = data.handles;
301
328
 
302
329
  styleSpecifier.annotationUID = annotationUID;
@@ -312,33 +339,60 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
312
339
 
313
340
  const radius = getCanvasCircleRadius(canvasCoordinates);
314
341
  const { centerPointRadius } = this.configuration;
315
-
342
+ const canvasCorners = getCanvasCircleCorners(canvasCoordinates);
316
343
  // range of slices to render based on the start and end slice, like
317
344
  // np.arange
318
345
 
319
- // if indexIJK is outside the start/end slice, we don't render
346
+ const focalPoint = viewport.getCamera().focalPoint;
347
+ const viewplaneNormal = viewport.getCamera().viewPlaneNormal;
348
+
349
+ let startCoord: number | vec3 = startCoordinate;
350
+ let endCoord: number | vec3 = endCoordinate;
351
+ if (Array.isArray(startCoordinate)) {
352
+ startCoord = this._getCoordinateForViewplaneNormal(
353
+ startCoord,
354
+ viewplaneNormal
355
+ );
356
+ }
357
+ if (Array.isArray(endCoordinate)) {
358
+ endCoord = this._getCoordinateForViewplaneNormal(
359
+ endCoord,
360
+ viewplaneNormal
361
+ );
362
+ }
363
+
364
+ const roundedStartCoord = coreUtils.roundToPrecision(startCoord);
365
+ const roundedEndCoord = coreUtils.roundToPrecision(endCoord);
366
+
367
+ const coord = this._getCoordinateForViewplaneNormal(
368
+ focalPoint,
369
+ viewplaneNormal
370
+ );
371
+ const roundedCoord = coreUtils.roundToPrecision(coord);
372
+
373
+ // if the focalpoint is outside the start/end coordinates, we don't render
320
374
  if (
321
- sliceIndex < Math.min(startSlice, endSlice) ||
322
- sliceIndex > Math.max(startSlice, endSlice)
375
+ roundedCoord < Math.min(roundedStartCoord, roundedEndCoord) ||
376
+ roundedCoord > Math.max(roundedStartCoord, roundedEndCoord)
323
377
  ) {
324
378
  continue;
325
379
  }
326
-
327
380
  // WE HAVE TO CACHE STATS BEFORE FETCHING TEXT
328
381
 
329
382
  if (annotation.invalidated) {
330
383
  this._throttledCalculateCachedStats(annotation, enabledElement);
331
384
  }
332
385
 
333
- const middleSlice = Math.round((startSlice + endSlice) / 2);
386
+ const middleCoord = coreUtils.roundToPrecision(
387
+ (startCoord + endCoord) / 2
388
+ );
334
389
  // if it is inside the start/end slice, but not exactly the first or
335
390
  // last slice, we render the line in dash, but not the handles
336
391
 
337
392
  let isMiddleSlice = false;
338
- if (sliceIndex === middleSlice) {
393
+ if (roundedCoord === middleCoord) {
339
394
  isMiddleSlice = true;
340
395
  }
341
-
342
396
  // If rendering engine has been destroyed while rendering
343
397
  if (!viewport.getRenderingEngine()) {
344
398
  console.warn('Rendering Engine has been destroyed');
@@ -350,7 +404,6 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
350
404
  if (!isAnnotationVisible(annotationUID)) {
351
405
  continue;
352
406
  }
353
-
354
407
  if (
355
408
  !isAnnotationLocked(annotation) &&
356
409
  !this.editData &&
@@ -376,9 +429,13 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
376
429
  }
377
430
 
378
431
  let lineWidthToUse = lineWidth;
432
+ let lineDashToUse = lineDash;
379
433
 
380
434
  if (isMiddleSlice) {
381
- lineWidthToUse = 3;
435
+ lineWidthToUse = lineWidth;
436
+ lineDashToUse = []; // Use solid line for real line
437
+ } else {
438
+ lineDashToUse = [5, 5]; // Use dashed line for projected lines
382
439
  }
383
440
 
384
441
  const circleUID = '0';
@@ -390,11 +447,10 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
390
447
  radius,
391
448
  {
392
449
  color,
393
- lineDash,
450
+ lineDash: lineDashToUse,
394
451
  lineWidth: lineWidthToUse,
395
452
  }
396
453
  );
397
-
398
454
  // draw center point, if "centerPointRadius" configuration is valid.
399
455
  if (centerPointRadius > 0) {
400
456
  if (radius > 3 * centerPointRadius) {
@@ -414,12 +470,69 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
414
470
  }
415
471
 
416
472
  renderStatus = true;
417
- }
418
473
 
474
+ if (
475
+ this.configuration.showTextBox == true &&
476
+ this.configuration.calculatePointsInsideVolume == true
477
+ ) {
478
+ const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
479
+ if (!options.visibility) {
480
+ data.handles.textBox = {
481
+ hasMoved: false,
482
+ worldPosition: <Types.Point3>[0, 0, 0],
483
+ worldBoundingBox: {
484
+ topLeft: <Types.Point3>[0, 0, 0],
485
+ topRight: <Types.Point3>[0, 0, 0],
486
+ bottomLeft: <Types.Point3>[0, 0, 0],
487
+ bottomRight: <Types.Point3>[0, 0, 0],
488
+ },
489
+ };
490
+ continue;
491
+ }
492
+ const textLines = this.configuration.getTextLines(data);
493
+ if (!textLines || textLines.length === 0) {
494
+ continue;
495
+ }
496
+
497
+ // Poor man's cached?
498
+ let canvasTextBoxCoords;
499
+
500
+ if (!data.handles.textBox.hasMoved) {
501
+ canvasTextBoxCoords = getTextBoxCoordsCanvas(canvasCorners);
502
+
503
+ data.handles.textBox.worldPosition =
504
+ viewport.canvasToWorld(canvasTextBoxCoords);
505
+ }
506
+
507
+ const textBoxPosition = viewport.worldToCanvas(
508
+ data.handles.textBox.worldPosition
509
+ );
510
+
511
+ const textBoxUID = '1';
512
+ const boundingBox = drawLinkedTextBoxSvg(
513
+ svgDrawingHelper,
514
+ annotationUID,
515
+ textBoxUID,
516
+ textLines,
517
+ textBoxPosition,
518
+ canvasCoordinates,
519
+ {},
520
+ options
521
+ );
522
+
523
+ const { x: left, y: top, width, height } = boundingBox;
524
+ data.handles.textBox.worldBoundingBox = {
525
+ topLeft: viewport.canvasToWorld([left, top]),
526
+ topRight: viewport.canvasToWorld([left + width, top]),
527
+ bottomLeft: viewport.canvasToWorld([left, top + height]),
528
+ bottomRight: viewport.canvasToWorld([left + width, top + height]),
529
+ };
530
+ }
531
+ }
419
532
  return renderStatus;
420
533
  };
421
534
 
422
- // Todo: make it work for planes other than acquisition planes
535
+ //Now works for axial, sagitall and coronal
423
536
  _computeProjectionPoints(
424
537
  annotation: CircleROIStartEndThresholdAnnotation,
425
538
  imageVolume: Types.IImageVolume
@@ -427,18 +540,13 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
427
540
  const { data, metadata } = annotation;
428
541
  const { viewPlaneNormal, spacingInNormal } = metadata;
429
542
  const { imageData } = imageVolume;
430
- const { startSlice, endSlice } = data;
543
+ const { startCoordinate, endCoordinate } = data;
431
544
  const { points } = data.handles;
432
545
 
433
546
  const startIJK = transformWorldToIndex(imageData, points[0]);
434
- startIJK[2] = startSlice;
435
-
436
- if (startIJK[2] !== startSlice) {
437
- throw new Error('Start slice does not match');
438
- }
547
+ const endIJK = transformWorldToIndex(imageData, points[0]);
439
548
 
440
- // substitute the end slice index 2 with startIJK index 2
441
- const endIJK = vec3.fromValues(startIJK[0], startIJK[1], endSlice);
549
+ const handlesToStart = cloneDeep(points);
442
550
 
443
551
  const startWorld = vec3.create();
444
552
  imageData.indexToWorldVec3(startIJK, startWorld);
@@ -446,6 +554,29 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
446
554
  const endWorld = vec3.create();
447
555
  imageData.indexToWorldVec3(endIJK, endWorld);
448
556
 
557
+ // substitute the end slice index 2 with startIJK index 2
558
+
559
+ if (this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal) == 2) {
560
+ startWorld[2] = startCoordinate;
561
+ endWorld[2] = endCoordinate;
562
+ handlesToStart[0][2] = startCoordinate;
563
+ handlesToStart[1][2] = startCoordinate;
564
+ } else if (
565
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal) == 0
566
+ ) {
567
+ startWorld[0] = startCoordinate;
568
+ endWorld[0] = endCoordinate;
569
+ handlesToStart[0][0] = startCoordinate;
570
+ handlesToStart[1][0] = startCoordinate;
571
+ } else if (
572
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal) == 1
573
+ ) {
574
+ startWorld[1] = startCoordinate;
575
+ endWorld[1] = endCoordinate;
576
+ handlesToStart[0][1] = startCoordinate;
577
+ handlesToStart[1][1] = startCoordinate;
578
+ }
579
+
449
580
  // distance between start and end slice in the world coordinate
450
581
  const distance = vec3.distance(startWorld, endWorld);
451
582
 
@@ -454,7 +585,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
454
585
  const newProjectionPoints = [];
455
586
  for (let dist = 0; dist < distance; dist += spacingInNormal) {
456
587
  newProjectionPoints.push(
457
- points.map((point) => {
588
+ handlesToStart.map((point) => {
458
589
  const newPoint = vec3.create();
459
590
  //@ts-ignore
460
591
  vec3.scaleAndAdd(newPoint, point, viewPlaneNormal, dist);
@@ -466,13 +597,59 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
466
597
  data.cachedStats.projectionPoints = newProjectionPoints;
467
598
  }
468
599
 
469
- _computePointsInsideVolume(annotation, imageVolume, enabledElement) {
470
- const { data } = annotation;
471
- const { viewport } = enabledElement;
600
+ _computePointsInsideVolume(
601
+ annotation,
602
+ imageVolume,
603
+ targetId,
604
+ enabledElement
605
+ ) {
606
+ const { data, metadata } = annotation;
607
+ const { viewPlaneNormal, viewUp } = metadata;
608
+ const { viewport, renderingEngine } = enabledElement;
472
609
  const projectionPoints = data.cachedStats.projectionPoints;
473
610
 
474
611
  const pointsInsideVolume: Types.Point3[][] = [[]];
475
612
 
613
+ const image = this.getTargetIdImage(targetId, renderingEngine);
614
+
615
+ const canvasCoordinates = data.handles.points.map((p) =>
616
+ viewport.worldToCanvas(p)
617
+ );
618
+ const [topLeftCanvas, bottomRightCanvas] = <Array<Types.Point2>>(
619
+ getCanvasCircleCorners(canvasCoordinates)
620
+ );
621
+ const pos1 = viewport.canvasToWorld(topLeftCanvas);
622
+ const pos2 = viewport.canvasToWorld(bottomRightCanvas);
623
+
624
+ const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints(
625
+ viewPlaneNormal,
626
+ viewUp,
627
+ pos1,
628
+ pos2
629
+ );
630
+ const measureInfo = getCalibratedLengthUnitsAndScale(image, data.handles);
631
+ const aspect = getCalibratedAspect(image);
632
+ const area = Math.abs(
633
+ Math.PI *
634
+ (worldWidth / measureInfo.scale / 2) *
635
+ (worldHeight / aspect / measureInfo.scale / 2)
636
+ );
637
+
638
+ const modalityUnitOptions = {
639
+ isPreScaled: isViewportPreScaled(viewport, targetId),
640
+ isSuvScaled: this.isSuvScaled(
641
+ viewport,
642
+ targetId,
643
+ annotation.metadata.referencedImageId
644
+ ),
645
+ };
646
+
647
+ const modalityUnit = getModalityUnit(
648
+ metadata.Modality,
649
+ annotation.metadata.referencedImageId,
650
+ modalityUnitOptions
651
+ );
652
+
476
653
  for (let i = 0; i < projectionPoints.length; i++) {
477
654
  // If image does not exists for the targetId, skip. This can be due
478
655
  // to various reasons such as if the target was a volumeViewport, and
@@ -499,17 +676,30 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
499
676
  const { dimensions, imageData } = imageVolume;
500
677
 
501
678
  const worldPos1Index = transformWorldToIndex(imageData, worldPos1);
502
- const worldCenterIndex = transformWorldToIndex(imageData, centerWorld);
679
+
680
+ const worldProjectionPointIndex = transformWorldToIndex(
681
+ imageData,
682
+ centerWorld
683
+ );
684
+
685
+ const indexOfProjection =
686
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal);
503
687
 
504
688
  worldPos1Index[0] = Math.floor(worldPos1Index[0]);
505
689
  worldPos1Index[1] = Math.floor(worldPos1Index[1]);
506
- worldPos1Index[2] = Math.floor(worldCenterIndex[2]);
690
+ worldPos1Index[2] = Math.floor(worldPos1Index[2]);
691
+
692
+ worldPos1Index[indexOfProjection] =
693
+ worldProjectionPointIndex[indexOfProjection];
507
694
 
508
695
  const worldPos2Index = transformWorldToIndex(imageData, worldPos2);
509
696
 
510
697
  worldPos2Index[0] = Math.floor(worldPos2Index[0]);
511
698
  worldPos2Index[1] = Math.floor(worldPos2Index[1]);
512
- worldPos2Index[2] = Math.floor(worldCenterIndex[2]);
699
+ worldPos2Index[2] = Math.floor(worldPos2Index[2]);
700
+
701
+ worldPos2Index[indexOfProjection] =
702
+ worldProjectionPointIndex[indexOfProjection];
513
703
 
514
704
  // Check if one of the indexes are inside the volume, this then gives us
515
705
  // Some area to do stats over.
@@ -543,7 +733,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
543
733
  imageData,
544
734
  //@ts-ignore
545
735
  (pointLPS) => pointInEllipse(ellipseObj, pointLPS),
546
- null,
736
+ this.configuration.statsCalculator.statsCallback,
547
737
  boundsIJK
548
738
  );
549
739
 
@@ -551,12 +741,23 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
551
741
  pointsInsideVolume.push(pointsInShape);
552
742
  }
553
743
  }
744
+ const stats = this.configuration.statsCalculator.getStatistics();
554
745
  data.cachedStats.pointsInVolume = pointsInsideVolume;
746
+ data.cachedStats.statistics = {
747
+ Modality: metadata.Modality,
748
+ area,
749
+ mean: stats.mean?.value,
750
+ stdDev: stats.stdDev?.value,
751
+ max: stats.max?.value,
752
+ statsArray: stats.array,
753
+ areaUnit: measureInfo.areaUnits,
754
+ modalityUnit,
755
+ };
555
756
  }
556
757
 
557
758
  _calculateCachedStatsTool(annotation, enabledElement) {
558
759
  const data = annotation.data;
559
- const { viewportId, renderingEngineId, viewport } = enabledElement;
760
+ const { viewport } = enabledElement;
560
761
 
561
762
  const { cachedStats } = data;
562
763
  const targetId = this.getTargetId(viewport);
@@ -569,21 +770,12 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
569
770
 
570
771
  annotation.invalidated = false;
571
772
 
572
- // Dispatching annotation modified
573
- const eventType = Events.ANNOTATION_MODIFIED;
574
-
575
- const eventDetail: AnnotationModifiedEventDetail = {
576
- annotation,
577
- viewportId,
578
- renderingEngineId,
579
- };
580
- triggerEvent(eventTarget, eventType, eventDetail);
773
+ triggerAnnotationModified(annotation, viewport.element);
581
774
 
582
775
  return cachedStats;
583
776
  }
584
777
 
585
- _getStartSliceIndex(
586
- imageVolume: Types.IImageVolume,
778
+ _getStartCoordinate(
587
779
  worldPos: Types.Point3,
588
780
  spacingInNormal: number,
589
781
  viewPlaneNormal: Types.Point3
@@ -601,24 +793,22 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
601
793
  numSlicesToPropagateFromStart * -spacingInNormal
602
794
  );
603
795
 
604
- const imageIdIndex = this._getImageIdIndex(
605
- imageVolume,
796
+ const startCoord = this._getCoordinateForViewplaneNormal(
606
797
  startPos,
607
- spacingInNormal,
608
798
  viewPlaneNormal
609
799
  );
610
800
 
611
- return imageIdIndex;
801
+ return startCoord;
612
802
  }
613
803
 
614
- _getEndSliceIndex(
615
- imageVolume: Types.IImageVolume,
804
+ _getEndCoordinate(
616
805
  worldPos: Types.Point3,
617
806
  spacingInNormal: number,
618
807
  viewPlaneNormal: Types.Point3
619
808
  ): number | undefined {
620
809
  const numSlicesToPropagate = this.configuration.numSlicesToPropagate;
621
- const numSlicesToPropagateFromStart = Math.round(numSlicesToPropagate / 2);
810
+ const numSlicesToPropagateToEnd =
811
+ numSlicesToPropagate - Math.round(numSlicesToPropagate / 2);
622
812
 
623
813
  // get end position by moving from worldPos in the direction of viewplaneNormal
624
814
  // with amount of numSlicesToPropagate * spacingInNormal
@@ -627,49 +817,67 @@ class CircleROIStartEndThresholdTool extends CircleROITool {
627
817
  endPos,
628
818
  worldPos,
629
819
  viewPlaneNormal,
630
- numSlicesToPropagateFromStart * spacingInNormal
820
+ numSlicesToPropagateToEnd * spacingInNormal
631
821
  );
632
822
 
633
- const imageIdIndex = this._getImageIdIndex(
634
- imageVolume,
823
+ const endCoord = this._getCoordinateForViewplaneNormal(
635
824
  endPos,
636
- spacingInNormal,
637
825
  viewPlaneNormal
638
826
  );
639
827
 
640
- return imageIdIndex;
828
+ return endCoord;
641
829
  }
642
830
 
643
- _getImageIdIndex(
644
- imageVolume: Types.IImageVolume,
645
- pos: vec3,
646
- spacingInNormal: number,
831
+ _getIndexOfCoordinatesForViewplaneNormal(
832
+ viewPlaneNormal: Types.Point3
833
+ ): number {
834
+ const viewplaneNormalAbs = [
835
+ Math.abs(viewPlaneNormal[0]),
836
+ Math.abs(viewPlaneNormal[1]),
837
+ Math.abs(viewPlaneNormal[2]),
838
+ ];
839
+ const indexOfDirection = viewplaneNormalAbs.indexOf(
840
+ Math.max(...viewplaneNormalAbs)
841
+ );
842
+
843
+ return indexOfDirection;
844
+ }
845
+
846
+ _getCoordinateForViewplaneNormal(
847
+ pos: vec3 | number,
647
848
  viewPlaneNormal: Types.Point3
648
849
  ): number | undefined {
649
- const halfSpacingInNormalDirection = spacingInNormal / 2;
650
- // Loop through imageIds of the imageVolume and find the one that is closest to endPos
651
- const { imageIds } = imageVolume;
652
- let imageIdIndex;
653
- for (let i = 0; i < imageIds.length; i++) {
654
- const imageId = imageIds[i];
655
-
656
- const { imagePositionPatient } = metaData.get(
657
- 'imagePlaneModule',
658
- imageId
659
- );
850
+ const indexOfDirection =
851
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal);
660
852
 
661
- const dir = vec3.create();
662
- vec3.sub(dir, pos, imagePositionPatient);
853
+ return pos[indexOfDirection];
854
+ }
855
+ }
663
856
 
664
- const dot = vec3.dot(dir, viewPlaneNormal);
857
+ /**
858
+ * _getTextLines - Returns the Area, mean and std deviation of the area of the
859
+ * target volume enclosed by the rectangle.
860
+ *
861
+ * @param data - The annotation tool-specific data.
862
+ * @param targetId - The volumeId of the volume to display the stats for.
863
+ */
864
+ function defaultGetTextLines(data): string[] {
865
+ const cachedVolumeStats = data.cachedStats.statistics;
665
866
 
666
- if (Math.abs(dot) < halfSpacingInNormalDirection) {
667
- imageIdIndex = i;
668
- }
669
- }
867
+ const { area, mean, max, stdDev, areaUnit, modalityUnit } = cachedVolumeStats;
670
868
 
671
- return imageIdIndex;
869
+ if (mean === undefined) {
870
+ return;
672
871
  }
872
+
873
+ const textLines: string[] = [];
874
+
875
+ textLines.push(`Area: ${roundNumber(area)} ${areaUnit}`);
876
+ textLines.push(`Mean: ${roundNumber(mean)} ${modalityUnit}`);
877
+ textLines.push(`Max: ${roundNumber(max)} ${modalityUnit}`);
878
+ textLines.push(`Std Dev: ${roundNumber(stdDev)} ${modalityUnit}`);
879
+
880
+ return textLines;
673
881
  }
674
882
 
675
883
  CircleROIStartEndThresholdTool.toolName = 'CircleROIStartEndThreshold';