@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
@@ -2,11 +2,11 @@ import {
2
2
  getEnabledElement,
3
3
  cache,
4
4
  StackViewport,
5
- metaData,
6
5
  utilities as csUtils,
7
6
  } from '@cornerstonejs/core';
8
- import type { Types } from '@cornerstonejs/core';
7
+ import { Types, utilities as coreUtils } from '@cornerstonejs/core';
9
8
 
9
+ import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits';
10
10
  import { vec3 } from 'gl-matrix';
11
11
  import {
12
12
  addAnnotation,
@@ -14,20 +14,26 @@ import {
14
14
  removeAnnotation,
15
15
  } from '../../stateManagement';
16
16
  import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
17
- import { triggerAnnotationModified } from '../../stateManagement/annotation/helpers/state';
18
17
  import {
19
18
  drawHandles as drawHandlesSvg,
20
19
  drawRect as drawRectSvg,
20
+ drawLinkedTextBox as drawLinkedTextBoxSvg,
21
21
  } from '../../drawingSvg';
22
22
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
23
23
  import throttle from '../../utilities/throttle';
24
+ import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
25
+ import getWorldWidthAndHeightFromCorners from '../../utilities/planar/getWorldWidthAndHeightFromCorners';
26
+
24
27
  import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
25
28
  import {
26
29
  hideElementCursor,
27
30
  resetElementCursor,
28
31
  } from '../../cursors/elementCursor';
29
32
  import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
30
- import { triggerAnnotationCompleted } from '../../stateManagement/annotation/helpers/state';
33
+ import {
34
+ triggerAnnotationCompleted,
35
+ triggerAnnotationModified,
36
+ } from '../../stateManagement/annotation/helpers/state';
31
37
 
32
38
  import {
33
39
  PublicToolProps,
@@ -38,7 +44,11 @@ import {
38
44
  import { RectangleROIStartEndThresholdAnnotation } from '../../types/ToolSpecificAnnotationTypes';
39
45
  import RectangleROITool from '../annotation/RectangleROITool';
40
46
  import { StyleSpecifier } from '../../types/AnnotationStyle';
41
- import { pointInShapeCallback } from '../../utilities/';
47
+ import { pointInShapeCallback, roundNumber } from '../../utilities/';
48
+ import { getModalityUnit } from '../../utilities/getModalityUnit';
49
+ import { isViewportPreScaled } from '../../utilities/viewport/isViewportPreScaled';
50
+ import { BasicStatsCalculator } from '../../utilities/math/basic';
51
+ import { filterAnnotationsWithinSamePlane } from '../../utilities/planar';
42
52
 
43
53
  const { transformWorldToIndex } = csUtils;
44
54
 
@@ -73,6 +83,9 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
73
83
  configuration: {
74
84
  numSlicesToPropagate: 10,
75
85
  computePointsInsideVolume: false,
86
+ getTextLines: defaultGetTextLines,
87
+ statsCalculator: BasicStatsCalculator,
88
+ showTextBox: false,
76
89
  },
77
90
  }
78
91
  ) {
@@ -120,22 +133,18 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
120
133
  );
121
134
  }
122
135
 
123
- if (!referencedImageId) {
124
- throw new Error('This tool does not work on non-acquisition planes');
125
- }
126
-
127
- const startIndex = viewport.getCurrentImageIdIndex();
128
136
  const spacingInNormal = csUtils.getSpacingInNormalDirection(
129
137
  imageVolume,
130
138
  viewPlaneNormal
131
139
  );
132
140
 
141
+ const startCoord = this._getStartCoordinate(worldPos, viewPlaneNormal);
142
+
133
143
  // We cannot simply add numSlicesToPropagate to startIndex because
134
144
  // the order of imageIds can be from top to bottom or bottom to top and
135
145
  // we want to make sure it is always propagated in the direction of the
136
146
  // view and also to make sure we don't go out of bounds.
137
- const endIndex = this._getEndSliceIndex(
138
- imageVolume,
147
+ const endCoord = this._getEndCoordinate(
139
148
  worldPos,
140
149
  spacingInNormal,
141
150
  viewPlaneNormal
@@ -158,19 +167,24 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
158
167
  },
159
168
  data: {
160
169
  label: '',
161
- startSlice: startIndex,
162
- endSlice: endIndex,
170
+ startCoordinate: startCoord,
171
+ endCoordinate: endCoord,
163
172
  cachedStats: {
164
173
  pointsInVolume: [],
165
174
  projectionPoints: [],
166
175
  projectionPointsImageIds: [referencedImageId],
176
+ statistics: [],
167
177
  },
168
178
  handles: {
169
- // No need a textBox
170
179
  textBox: {
171
180
  hasMoved: false,
172
- worldPosition: null,
173
- worldBoundingBox: null,
181
+ worldPosition: <Types.Point3>[0, 0, 0],
182
+ worldBoundingBox: {
183
+ topLeft: <Types.Point3>[0, 0, 0],
184
+ topRight: <Types.Point3>[0, 0, 0],
185
+ bottomLeft: <Types.Point3>[0, 0, 0],
186
+ bottomRight: <Types.Point3>[0, 0, 0],
187
+ },
174
188
  },
175
189
  points: [
176
190
  <Types.Point3>[...worldPos],
@@ -249,7 +263,12 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
249
263
  const imageVolume = cache.getVolume(targetId.split(/volumeId:|\?/)[1]);
250
264
 
251
265
  if (this.configuration.calculatePointsInsideVolume) {
252
- this._computePointsInsideVolume(annotation, imageVolume, enabledElement);
266
+ this._computePointsInsideVolume(
267
+ annotation,
268
+ targetId,
269
+ imageVolume,
270
+ enabledElement
271
+ );
253
272
  }
254
273
 
255
274
  triggerAnnotationRenderForViewportIds(
@@ -262,7 +281,7 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
262
281
  }
263
282
  };
264
283
 
265
- // Todo: make it work for planes other than acquisition planes
284
+ //Now works for non-acquisition planes
266
285
  _computeProjectionPoints(
267
286
  annotation: RectangleROIStartEndThresholdAnnotation,
268
287
  imageVolume: Types.IImageVolume
@@ -270,17 +289,11 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
270
289
  const { data, metadata } = annotation;
271
290
  const { viewPlaneNormal, spacingInNormal } = metadata;
272
291
  const { imageData } = imageVolume;
273
- const { startSlice, endSlice } = data;
292
+ const { startCoordinate, endCoordinate } = data;
274
293
  const { points } = data.handles;
275
294
 
276
295
  const startIJK = transformWorldToIndex(imageData, points[0]);
277
-
278
- if (startIJK[2] !== startSlice) {
279
- throw new Error('Start slice does not match');
280
- }
281
-
282
- // substitute the end slice index 2 with startIJK index 2
283
- const endIJK = vec3.fromValues(startIJK[0], startIJK[1], endSlice);
296
+ const endIJK = transformWorldToIndex(imageData, points[0]);
284
297
 
285
298
  const startWorld = vec3.create();
286
299
  imageData.indexToWorldVec3(startIJK, startWorld);
@@ -288,6 +301,23 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
288
301
  const endWorld = vec3.create();
289
302
  imageData.indexToWorldVec3(endIJK, endWorld);
290
303
 
304
+ // substitute the end slice index 2 with startIJK index 2
305
+
306
+ if (this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal) == 2) {
307
+ startWorld[2] = startCoordinate;
308
+ endWorld[2] = endCoordinate;
309
+ } else if (
310
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal) == 0
311
+ ) {
312
+ startWorld[0] = startCoordinate;
313
+ endWorld[0] = endCoordinate;
314
+ } else if (
315
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal) == 1
316
+ ) {
317
+ startWorld[1] = startCoordinate;
318
+ endWorld[1] = endCoordinate;
319
+ }
320
+
291
321
  // distance between start and end slice in the world coordinate
292
322
  const distance = vec3.distance(startWorld, endWorld);
293
323
 
@@ -298,6 +328,7 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
298
328
  newProjectionPoints.push(
299
329
  points.map((point) => {
300
330
  const newPoint = vec3.create();
331
+ //@ts-ignore
301
332
  vec3.scaleAndAdd(newPoint, point, viewPlaneNormal, dist);
302
333
  return Array.from(newPoint);
303
334
  })
@@ -305,27 +336,54 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
305
336
  }
306
337
 
307
338
  data.cachedStats.projectionPoints = newProjectionPoints;
308
-
309
- // Find the imageIds for the projection points
310
- const projectionPointsImageIds = [];
311
- for (const RectanglePoints of newProjectionPoints) {
312
- const imageId = csUtils.getClosestImageId(
313
- imageVolume,
314
- RectanglePoints[0],
315
- viewPlaneNormal
316
- );
317
- projectionPointsImageIds.push(imageId);
318
- }
319
-
320
- data.cachedStats.projectionPointsImageIds = projectionPointsImageIds;
321
339
  }
322
340
 
323
- //This function return all the points inside the ROI for every slices between startSlice and endSlice
324
- _computePointsInsideVolume(annotation, imageVolume, enabledElement) {
325
- const { data } = annotation;
341
+ //This function return all the points inside the ROI and calculate statistics for every slices between startCoordinate and endCoordinate
342
+ _computePointsInsideVolume(
343
+ annotation,
344
+ targetId,
345
+ imageVolume,
346
+ enabledElement
347
+ ) {
348
+ const { data, metadata } = annotation;
349
+ const { viewPlaneNormal, viewUp } = metadata;
350
+ const { viewport, renderingEngine } = enabledElement;
351
+
326
352
  const projectionPoints = data.cachedStats.projectionPoints;
327
353
 
328
354
  const pointsInsideVolume: Types.Point3[][] = [[]];
355
+ const image = this.getTargetIdImage(targetId, renderingEngine);
356
+
357
+ const worldPos1 = data.handles.points[0];
358
+ const worldPos2 = data.handles.points[3];
359
+
360
+ const { worldWidth, worldHeight } = getWorldWidthAndHeightFromCorners(
361
+ viewPlaneNormal,
362
+ viewUp,
363
+ worldPos1,
364
+ worldPos2
365
+ );
366
+ const measureInfo = getCalibratedLengthUnitsAndScale(image, data.habdles);
367
+
368
+ const area =
369
+ Math.abs(worldWidth * worldHeight) /
370
+ (measureInfo.scale * measureInfo.scale);
371
+
372
+ const modalityUnitOptions = {
373
+ isPreScaled: isViewportPreScaled(viewport, targetId),
374
+
375
+ isSuvScaled: this.isSuvScaled(
376
+ viewport,
377
+ targetId,
378
+ annotation.metadata.referencedImageId
379
+ ),
380
+ };
381
+
382
+ const modalityUnit = getModalityUnit(
383
+ metadata.Modality,
384
+ annotation.metadata.referencedImageId,
385
+ modalityUnitOptions
386
+ );
329
387
 
330
388
  for (let i = 0; i < projectionPoints.length; i++) {
331
389
  // If image does not exists for the targetId, skip. This can be due
@@ -337,9 +395,6 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
337
395
 
338
396
  const projectionPoint = projectionPoints[i][0];
339
397
 
340
- const worldPos1 = data.handles.points[0];
341
- const worldPos2 = data.handles.points[3];
342
-
343
398
  const { dimensions, imageData } = imageVolume;
344
399
 
345
400
  const worldPos1Index = transformWorldToIndex(imageData, worldPos1);
@@ -349,15 +404,24 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
349
404
  projectionPoint
350
405
  );
351
406
 
407
+ const indexOfProjection =
408
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal);
409
+
352
410
  worldPos1Index[0] = Math.floor(worldPos1Index[0]);
353
411
  worldPos1Index[1] = Math.floor(worldPos1Index[1]);
354
- worldPos1Index[2] = Math.floor(worldProjectionPointIndex[2]);
412
+ worldPos1Index[2] = Math.floor(worldPos1Index[2]);
413
+
414
+ worldPos1Index[indexOfProjection] =
415
+ worldProjectionPointIndex[indexOfProjection];
355
416
 
356
417
  const worldPos2Index = transformWorldToIndex(imageData, worldPos2);
357
418
 
358
419
  worldPos2Index[0] = Math.floor(worldPos2Index[0]);
359
420
  worldPos2Index[1] = Math.floor(worldPos2Index[1]);
360
- worldPos2Index[2] = Math.floor(worldProjectionPointIndex[2]);
421
+ worldPos2Index[2] = Math.floor(worldPos2Index[2]);
422
+
423
+ worldPos2Index[indexOfProjection] =
424
+ worldProjectionPointIndex[indexOfProjection];
361
425
 
362
426
  // Check if one of the indexes are inside the volume, this then gives us
363
427
  // Some area to do stats over.
@@ -382,7 +446,7 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
382
446
  const pointsInShape = pointInShapeCallback(
383
447
  imageData,
384
448
  () => true,
385
- null,
449
+ this.configuration.statsCalculator.statsCallback,
386
450
  boundsIJK
387
451
  );
388
452
 
@@ -390,7 +454,18 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
390
454
  pointsInsideVolume.push(pointsInShape);
391
455
  }
392
456
  }
457
+ const stats = this.configuration.statsCalculator.getStatistics();
393
458
  data.cachedStats.pointsInVolume = pointsInsideVolume;
459
+ data.cachedStats.statistics = {
460
+ Modality: metadata.Modality,
461
+ area,
462
+ mean: stats.mean?.value,
463
+ stdDev: stats.stdDev?.value,
464
+ max: stats.max?.value,
465
+ statsArray: stats.array,
466
+ areaUnit: measureInfo.areaUnits,
467
+ modalityUnit,
468
+ };
394
469
  }
395
470
 
396
471
  _calculateCachedStatsTool(annotation, enabledElement) {
@@ -427,14 +502,16 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
427
502
  ): boolean => {
428
503
  let renderStatus = false;
429
504
  const { viewport } = enabledElement;
430
-
431
- const annotations = getAnnotations(this.getToolName(), viewport.element);
505
+ let annotations = getAnnotations(this.getToolName(), viewport.element);
432
506
 
433
507
  if (!annotations?.length) {
434
508
  return renderStatus;
435
509
  }
436
510
 
437
- const sliceIndex = viewport.getCurrentImageIdIndex();
511
+ annotations = filterAnnotationsWithinSamePlane(
512
+ annotations,
513
+ viewport.getCamera()
514
+ );
438
515
 
439
516
  const styleSpecifier: StyleSpecifier = {
440
517
  toolGroupId: this.toolGroupId,
@@ -447,7 +524,7 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
447
524
  i
448
525
  ] as RectangleROIStartEndThresholdAnnotation;
449
526
  const { annotationUID, data } = annotation;
450
- const { startSlice, endSlice } = data;
527
+ const { startCoordinate, endCoordinate } = data;
451
528
  const { points, activeHandleIndex } = data.handles;
452
529
 
453
530
  const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
@@ -460,10 +537,37 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
460
537
  // range of slices to render based on the start and end slice, like
461
538
  // np.arange
462
539
 
463
- // if indexIJK is outside the start/end slice, we don't render
540
+ const focalPoint = viewport.getCamera().focalPoint;
541
+ const viewplaneNormal = viewport.getCamera().viewPlaneNormal;
542
+
543
+ let startCoord: number | vec3 = startCoordinate;
544
+ let endCoord: number | vec3 = endCoordinate;
545
+ if (Array.isArray(startCoordinate)) {
546
+ startCoord = this._getCoordinateForViewplaneNormal(
547
+ startCoord,
548
+ viewplaneNormal
549
+ );
550
+ }
551
+
552
+ if (Array.isArray(endCoordinate)) {
553
+ endCoord = this._getCoordinateForViewplaneNormal(
554
+ endCoord,
555
+ viewplaneNormal
556
+ );
557
+ }
558
+
559
+ const roundedStartCoord = coreUtils.roundToPrecision(startCoord);
560
+ const roundedEndCoord = coreUtils.roundToPrecision(endCoord);
561
+
562
+ const coord = this._getCoordinateForViewplaneNormal(
563
+ focalPoint,
564
+ viewplaneNormal
565
+ );
566
+ const roundedCoord = coreUtils.roundToPrecision(coord);
567
+ // if the focalpoint is outside the start/end coordinates, we don't render
464
568
  if (
465
- sliceIndex < Math.min(startSlice, endSlice) ||
466
- sliceIndex > Math.max(startSlice, endSlice)
569
+ roundedCoord < Math.min(roundedStartCoord, roundedEndCoord) ||
570
+ roundedCoord > Math.max(roundedStartCoord, roundedEndCoord)
467
571
  ) {
468
572
  continue;
469
573
  }
@@ -477,7 +581,10 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
477
581
  // if it is inside the start/end slice, but not exactly the first or
478
582
  // last slice, we render the line in dash, but not the handles
479
583
  let firstOrLastSlice = false;
480
- if (sliceIndex === startSlice || sliceIndex === endSlice) {
584
+ if (
585
+ roundedCoord === roundedStartCoord ||
586
+ roundedCoord === roundedEndCoord
587
+ ) {
481
588
  firstOrLastSlice = true;
482
589
  }
483
590
 
@@ -538,13 +645,82 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
538
645
  );
539
646
 
540
647
  renderStatus = true;
648
+
649
+ if (
650
+ this.configuration.showTextBox &&
651
+ this.configuration.calculatePointsInsideVolume
652
+ ) {
653
+ const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
654
+ if (!options.visibility) {
655
+ data.handles.textBox = {
656
+ hasMoved: false,
657
+ worldPosition: <Types.Point3>[0, 0, 0],
658
+ worldBoundingBox: {
659
+ topLeft: <Types.Point3>[0, 0, 0],
660
+ topRight: <Types.Point3>[0, 0, 0],
661
+ bottomLeft: <Types.Point3>[0, 0, 0],
662
+ bottomRight: <Types.Point3>[0, 0, 0],
663
+ },
664
+ };
665
+ continue;
666
+ }
667
+
668
+ const textLines = this.configuration.getTextLines(data);
669
+ if (!textLines || textLines.length === 0) {
670
+ continue;
671
+ }
672
+
673
+ if (!data.handles.textBox.hasMoved) {
674
+ const canvasTextBoxCoords = getTextBoxCoordsCanvas(canvasCoordinates);
675
+
676
+ data.handles.textBox.worldPosition =
677
+ viewport.canvasToWorld(canvasTextBoxCoords);
678
+ }
679
+
680
+ const textBoxPosition = viewport.worldToCanvas(
681
+ data.handles.textBox.worldPosition
682
+ );
683
+
684
+ const textBoxUID = '1';
685
+ const boundingBox = drawLinkedTextBoxSvg(
686
+ svgDrawingHelper,
687
+ annotationUID,
688
+ textBoxUID,
689
+ textLines,
690
+ textBoxPosition,
691
+ canvasCoordinates,
692
+ {},
693
+ options
694
+ );
695
+
696
+ const { x: left, y: top, width, height } = boundingBox;
697
+
698
+ data.handles.textBox.worldBoundingBox = {
699
+ topLeft: viewport.canvasToWorld([left, top]),
700
+ topRight: viewport.canvasToWorld([left + width, top]),
701
+ bottomLeft: viewport.canvasToWorld([left, top + height]),
702
+ bottomRight: viewport.canvasToWorld([left + width, top + height]),
703
+ };
704
+ }
541
705
  }
542
706
 
543
707
  return renderStatus;
544
708
  };
545
709
 
546
- _getEndSliceIndex(
547
- imageVolume: Types.IImageVolume,
710
+ _getStartCoordinate(
711
+ worldPos: Types.Point3,
712
+ viewPlaneNormal: Types.Point3
713
+ ): number | undefined {
714
+ const startPos = worldPos;
715
+ const startCoord = this._getCoordinateForViewplaneNormal(
716
+ startPos,
717
+ viewPlaneNormal
718
+ );
719
+
720
+ return startCoord;
721
+ }
722
+
723
+ _getEndCoordinate(
548
724
  worldPos: Types.Point3,
549
725
  spacingInNormal: number,
550
726
  viewPlaneNormal: Types.Point3
@@ -561,31 +737,65 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool {
561
737
  numSlicesToPropagate * spacingInNormal
562
738
  );
563
739
 
564
- const halfSpacingInNormalDirection = spacingInNormal / 2;
565
- // Loop through imageIds of the imageVolume and find the one that is closest to endPos
566
- const { imageIds } = imageVolume;
567
- let imageIdIndex;
568
- for (let i = 0; i < imageIds.length; i++) {
569
- const imageId = imageIds[i];
740
+ const endCoord = this._getCoordinateForViewplaneNormal(
741
+ endPos,
742
+ viewPlaneNormal
743
+ );
570
744
 
571
- const { imagePositionPatient } = metaData.get(
572
- 'imagePlaneModule',
573
- imageId
574
- );
745
+ return endCoord;
746
+ }
575
747
 
576
- const dir = vec3.create();
577
- vec3.sub(dir, endPos, imagePositionPatient);
748
+ _getIndexOfCoordinatesForViewplaneNormal(
749
+ viewPlaneNormal: Types.Point3
750
+ ): number {
751
+ const viewplaneNormalAbs = [
752
+ Math.abs(viewPlaneNormal[0]),
753
+ Math.abs(viewPlaneNormal[1]),
754
+ Math.abs(viewPlaneNormal[2]),
755
+ ];
756
+ const indexOfDirection = viewplaneNormalAbs.indexOf(
757
+ Math.max(...viewplaneNormalAbs)
758
+ );
578
759
 
579
- const dot = vec3.dot(dir, viewPlaneNormal);
760
+ return indexOfDirection;
761
+ }
580
762
 
581
- if (Math.abs(dot) < halfSpacingInNormalDirection) {
582
- imageIdIndex = i;
583
- }
584
- }
763
+ _getCoordinateForViewplaneNormal(
764
+ pos: vec3 | number,
765
+ viewPlaneNormal: Types.Point3
766
+ ): number | undefined {
767
+ const indexOfDirection =
768
+ this._getIndexOfCoordinatesForViewplaneNormal(viewPlaneNormal);
585
769
 
586
- return imageIdIndex;
770
+ return pos[indexOfDirection];
587
771
  }
588
772
  }
589
773
 
774
+ /**
775
+ * _getTextLines - Returns the Area, mean and std deviation of the area of the
776
+ * target volume enclosed by the rectangle.
777
+ *
778
+ * @param data - The annotation tool-specific data.
779
+ * @param targetId - The volumeId of the volume to display the stats for.
780
+ */
781
+ function defaultGetTextLines(data): string[] {
782
+ const cachedVolumeStats = data.cachedStats.statistics;
783
+
784
+ const { area, mean, max, stdDev, areaUnit, modalityUnit } = cachedVolumeStats;
785
+
786
+ if (mean === undefined) {
787
+ return;
788
+ }
789
+
790
+ const textLines: string[] = [];
791
+
792
+ textLines.push(`Area: ${roundNumber(area)} ${areaUnit}`);
793
+ textLines.push(`Mean: ${roundNumber(mean)} ${modalityUnit}`);
794
+ textLines.push(`Max: ${roundNumber(max)} ${modalityUnit}`);
795
+ textLines.push(`Std Dev: ${roundNumber(stdDev)} ${modalityUnit}`);
796
+
797
+ return textLines;
798
+ }
799
+
590
800
  RectangleROIStartEndThresholdTool.toolName = 'RectangleROIStartEndThreshold';
591
801
  export default RectangleROIStartEndThresholdTool;
@@ -240,16 +240,27 @@ export interface RectangleROIStartEndThresholdAnnotation extends Annotation {
240
240
  };
241
241
  data: {
242
242
  label: string;
243
- startSlice: number;
244
- endSlice: number;
243
+ startCoordinate: number;
244
+ endCoordinate: number;
245
245
  cachedStats: {
246
246
  pointsInVolume: Types.Point3[];
247
247
  projectionPoints: Types.Point3[][]; // first slice p1, p2, p3, p4; second slice p1, p2, p3, p4 ...
248
248
  projectionPointsImageIds: string[];
249
+ statistics?: ROICachedStats | any[];
249
250
  };
250
251
  handles: {
251
252
  points: Types.Point3[];
252
253
  activeHandleIndex: number | null;
254
+ textBox: {
255
+ hasMoved: boolean;
256
+ worldPosition: Types.Point3;
257
+ worldBoundingBox: {
258
+ topLeft: Types.Point3;
259
+ topRight: Types.Point3;
260
+ bottomLeft: Types.Point3;
261
+ bottomRight: Types.Point3;
262
+ };
263
+ };
253
264
  };
254
265
  };
255
266
  }
@@ -270,15 +281,26 @@ export interface CircleROIStartEndThresholdAnnotation extends Annotation {
270
281
  };
271
282
  data: {
272
283
  label: string;
273
- startSlice: number;
274
- endSlice: number;
284
+ startCoordinate: number;
285
+ endCoordinate: number;
275
286
  cachedStats?: {
276
287
  pointsInVolume: Types.Point3[];
277
288
  projectionPoints: Types.Point3[][];
289
+ statistics?: ROICachedStats | any[];
278
290
  };
279
291
  handles: {
280
292
  points: [Types.Point3, Types.Point3]; // [center, end]
281
293
  activeHandleIndex: number | null;
294
+ textBox?: {
295
+ hasMoved: boolean;
296
+ worldPosition: Types.Point3;
297
+ worldBoundingBox: {
298
+ topLeft: Types.Point3;
299
+ topRight: Types.Point3;
300
+ bottomLeft: Types.Point3;
301
+ bottomRight: Types.Point3;
302
+ };
303
+ };
282
304
  };
283
305
  };
284
306
  }