@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.
- package/dist/cjs/cursors/SVGCursorDescriptor.js +7 -0
- package/dist/cjs/cursors/SVGCursorDescriptor.js.map +1 -1
- package/dist/cjs/drawingSvg/drawHeight.d.ts +3 -0
- package/dist/cjs/drawingSvg/drawHeight.js +49 -0
- package/dist/cjs/drawingSvg/drawHeight.js.map +1 -0
- package/dist/cjs/drawingSvg/index.d.ts +2 -1
- package/dist/cjs/drawingSvg/index.js +3 -1
- package/dist/cjs/drawingSvg/index.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tools/annotation/HeightTool.d.ts +40 -0
- package/dist/cjs/tools/annotation/HeightTool.js +463 -0
- package/dist/cjs/tools/annotation/HeightTool.js.map +1 -0
- package/dist/cjs/tools/index.d.ts +2 -1
- package/dist/cjs/tools/index.js +4 -2
- package/dist/cjs/tools/index.js.map +1 -1
- package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +15 -8
- package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js +189 -62
- package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -1
- package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +17 -7
- package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js +167 -52
- package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
- package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +26 -4
- package/dist/cjs/utilities/planar/filterAnnotationsWithinPlane.d.ts +3 -0
- package/dist/cjs/utilities/planar/filterAnnotationsWithinPlane.js +31 -0
- package/dist/cjs/utilities/planar/filterAnnotationsWithinPlane.js.map +1 -0
- package/dist/cjs/utilities/planar/index.d.ts +3 -1
- package/dist/cjs/utilities/planar/index.js +4 -1
- package/dist/cjs/utilities/planar/index.js.map +1 -1
- package/dist/cjs/utilities/viewport/isViewportPreScaled.js +1 -4
- package/dist/cjs/utilities/viewport/isViewportPreScaled.js.map +1 -1
- package/dist/esm/cursors/SVGCursorDescriptor.js +7 -0
- package/dist/esm/cursors/SVGCursorDescriptor.js.map +1 -1
- package/dist/esm/drawingSvg/drawHeight.js +43 -0
- package/dist/esm/drawingSvg/drawHeight.js.map +1 -0
- package/dist/esm/drawingSvg/index.js +2 -1
- package/dist/esm/drawingSvg/index.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tools/annotation/HeightTool.js +439 -0
- package/dist/esm/tools/annotation/HeightTool.js.map +1 -0
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +192 -66
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -1
- package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js +168 -54
- package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
- package/dist/esm/utilities/planar/filterAnnotationsWithinPlane.js +27 -0
- package/dist/esm/utilities/planar/filterAnnotationsWithinPlane.js.map +1 -0
- package/dist/esm/utilities/planar/index.js +3 -1
- package/dist/esm/utilities/planar/index.js.map +1 -1
- package/dist/esm/utilities/viewport/isViewportPreScaled.js +2 -5
- package/dist/esm/utilities/viewport/isViewportPreScaled.js.map +1 -1
- package/dist/types/cursors/SVGCursorDescriptor.d.ts.map +1 -1
- package/dist/types/drawingSvg/drawHeight.d.ts +4 -0
- package/dist/types/drawingSvg/drawHeight.d.ts.map +1 -0
- package/dist/types/drawingSvg/index.d.ts +2 -1
- package/dist/types/drawingSvg/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/tools/annotation/HeightTool.d.ts +41 -0
- package/dist/types/tools/annotation/HeightTool.d.ts.map +1 -0
- package/dist/types/tools/index.d.ts +2 -1
- package/dist/types/tools/index.d.ts.map +1 -1
- package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +15 -8
- package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts.map +1 -1
- package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +17 -7
- package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts.map +1 -1
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts +26 -4
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
- package/dist/types/utilities/planar/filterAnnotationsWithinPlane.d.ts +4 -0
- package/dist/types/utilities/planar/filterAnnotationsWithinPlane.d.ts.map +1 -0
- package/dist/types/utilities/planar/index.d.ts +3 -1
- package/dist/types/utilities/planar/index.d.ts.map +1 -1
- package/dist/types/utilities/viewport/isViewportPreScaled.d.ts.map +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +3 -3
- package/src/cursors/SVGCursorDescriptor.ts +7 -0
- package/src/drawingSvg/drawHeight.ts +90 -0
- package/src/drawingSvg/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/tools/annotation/HeightTool.ts +882 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +310 -102
- package/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +287 -77
- package/src/types/ToolSpecificAnnotationTypes.ts +26 -4
- package/src/utilities/planar/filterAnnotationsWithinPlane.ts +76 -0
- package/src/utilities/planar/index.ts +3 -0
- 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
|
-
|
|
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 {
|
|
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
|
|
133
|
-
imageVolume,
|
|
143
|
+
const startCoord = this._getStartCoordinate(
|
|
134
144
|
worldPos,
|
|
135
145
|
spacingInNormal,
|
|
136
146
|
viewPlaneNormal
|
|
137
147
|
);
|
|
138
148
|
|
|
139
|
-
// We cannot
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
176
|
+
startCoordinate: startCoord,
|
|
177
|
+
endCoordinate: endCoord,
|
|
169
178
|
|
|
170
179
|
handles: {
|
|
171
180
|
textBox: {
|
|
172
181
|
hasMoved: false,
|
|
173
|
-
worldPosition:
|
|
174
|
-
worldBoundingBox:
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
-
//
|
|
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 {
|
|
543
|
+
const { startCoordinate, endCoordinate } = data;
|
|
431
544
|
const { points } = data.handles;
|
|
432
545
|
|
|
433
546
|
const startIJK = transformWorldToIndex(imageData, points[0]);
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
605
|
-
imageVolume,
|
|
796
|
+
const startCoord = this._getCoordinateForViewplaneNormal(
|
|
606
797
|
startPos,
|
|
607
|
-
spacingInNormal,
|
|
608
798
|
viewPlaneNormal
|
|
609
799
|
);
|
|
610
800
|
|
|
611
|
-
return
|
|
801
|
+
return startCoord;
|
|
612
802
|
}
|
|
613
803
|
|
|
614
|
-
|
|
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
|
|
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
|
-
|
|
820
|
+
numSlicesToPropagateToEnd * spacingInNormal
|
|
631
821
|
);
|
|
632
822
|
|
|
633
|
-
const
|
|
634
|
-
imageVolume,
|
|
823
|
+
const endCoord = this._getCoordinateForViewplaneNormal(
|
|
635
824
|
endPos,
|
|
636
|
-
spacingInNormal,
|
|
637
825
|
viewPlaneNormal
|
|
638
826
|
);
|
|
639
827
|
|
|
640
|
-
return
|
|
828
|
+
return endCoord;
|
|
641
829
|
}
|
|
642
830
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
650
|
-
|
|
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
|
-
|
|
662
|
-
|
|
853
|
+
return pos[indexOfDirection];
|
|
854
|
+
}
|
|
855
|
+
}
|
|
663
856
|
|
|
664
|
-
|
|
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
|
-
|
|
667
|
-
imageIdIndex = i;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
867
|
+
const { area, mean, max, stdDev, areaUnit, modalityUnit } = cachedVolumeStats;
|
|
670
868
|
|
|
671
|
-
|
|
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';
|