@cornerstonejs/tools 1.62.0 → 1.63.1

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 (39) hide show
  1. package/dist/cjs/index.d.ts +2 -2
  2. package/dist/cjs/index.js +2 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/tools/index.d.ts +2 -1
  5. package/dist/cjs/tools/index.js +4 -1
  6. package/dist/cjs/tools/index.js.map +1 -1
  7. package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +63 -0
  8. package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js +347 -0
  9. package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -0
  10. package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +3 -0
  11. package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js +73 -0
  12. package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
  13. package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +34 -1
  14. package/dist/esm/index.js +2 -2
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/tools/index.js +2 -1
  17. package/dist/esm/tools/index.js.map +1 -1
  18. package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +342 -0
  19. package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -0
  20. package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js +75 -2
  21. package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
  22. package/dist/types/index.d.ts +2 -2
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/types/tools/index.d.ts +2 -1
  25. package/dist/types/tools/index.d.ts.map +1 -1
  26. package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +64 -0
  27. package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts.map +1 -0
  28. package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +3 -0
  29. package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts.map +1 -1
  30. package/dist/types/types/ToolSpecificAnnotationTypes.d.ts +34 -1
  31. package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
  32. package/dist/umd/index.js +1 -1
  33. package/dist/umd/index.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/index.ts +2 -0
  36. package/src/tools/index.ts +2 -0
  37. package/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +677 -0
  38. package/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +134 -2
  39. package/src/types/ToolSpecificAnnotationTypes.ts +43 -8
@@ -0,0 +1,677 @@
1
+ import {
2
+ StackViewport,
3
+ Types,
4
+ cache,
5
+ getEnabledElement,
6
+ utilities as csUtils,
7
+ metaData,
8
+ triggerEvent,
9
+ eventTarget,
10
+ } from '@cornerstonejs/core';
11
+
12
+ import { vec3 } from 'gl-matrix';
13
+ import { Events } from '../../enums';
14
+ import {
15
+ addAnnotation,
16
+ removeAnnotation,
17
+ getAnnotations,
18
+ } from '../../stateManagement/annotation/annotationState';
19
+ import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
20
+ import {
21
+ drawCircle as drawCircleSvg,
22
+ drawHandles as drawHandlesSvg,
23
+ } from '../../drawingSvg';
24
+ import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
25
+ import throttle from '../../utilities/throttle';
26
+ import { AnnotationModifiedEventDetail } from '../../types/EventTypes';
27
+ import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
28
+ import {
29
+ hideElementCursor,
30
+ resetElementCursor,
31
+ } from '../../cursors/elementCursor';
32
+ import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
33
+ import { triggerAnnotationCompleted } from '../../stateManagement/annotation/helpers/state';
34
+ import {
35
+ PublicToolProps,
36
+ ToolProps,
37
+ EventTypes,
38
+ SVGDrawingHelper,
39
+ } from '../../types';
40
+ import { CircleROIStartEndThresholdAnnotation } from '../../types/ToolSpecificAnnotationTypes';
41
+ import CircleROITool from '../annotation/CircleROITool';
42
+ import { StyleSpecifier } from '../../types/AnnotationStyle';
43
+ import {
44
+ getCanvasCircleCorners,
45
+ getCanvasCircleRadius,
46
+ } from '../../utilities/math/circle';
47
+ import { pointInEllipse } from '../../utilities/math/ellipse';
48
+ import { pointInShapeCallback } from '../../utilities';
49
+
50
+ const { transformWorldToIndex } = csUtils;
51
+
52
+ class CircleROIStartEndThresholdTool extends CircleROITool {
53
+ static toolName;
54
+
55
+ touchDragCallback: any;
56
+ mouseDragCallback: any;
57
+ _throttledCalculateCachedStats: any;
58
+ editData: {
59
+ annotation: any;
60
+ viewportIdsToRender: Array<string>;
61
+ handleIndex?: number;
62
+ newAnnotation?: boolean;
63
+ hasMoved?: boolean;
64
+ } | null;
65
+ isDrawing: boolean;
66
+ isHandleOutsideImage = false;
67
+
68
+ constructor(
69
+ toolProps: PublicToolProps = {},
70
+ defaultToolProps: ToolProps = {
71
+ supportedInteractionTypes: ['Mouse', 'Touch'],
72
+ configuration: {
73
+ numSlicesToPropagate: 10,
74
+ calculatePointsInsideVolume: false,
75
+ },
76
+ }
77
+ ) {
78
+ super(toolProps, defaultToolProps);
79
+
80
+ this._throttledCalculateCachedStats = throttle(
81
+ this._calculateCachedStatsTool,
82
+ 100,
83
+ { trailing: true }
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Based on the current position of the mouse and the current imageId to create
89
+ * a CircleROI Annotation and stores it in the annotationManager
90
+ *
91
+ * @param evt - EventTypes.NormalizedMouseEventType
92
+ * @returns The annotation object.
93
+ *
94
+ */
95
+ addNewAnnotation = (evt: EventTypes.InteractionEventType) => {
96
+ const eventDetail = evt.detail;
97
+ const { currentPoints, element } = eventDetail;
98
+ const worldPos = currentPoints.world;
99
+ const canvasPos = currentPoints.canvas;
100
+
101
+ const enabledElement = getEnabledElement(element);
102
+ const { viewport, renderingEngine } = enabledElement;
103
+
104
+ this.isDrawing = true;
105
+
106
+ const camera = viewport.getCamera();
107
+ const { viewPlaneNormal, viewUp } = camera;
108
+
109
+ let referencedImageId, imageVolume, volumeId;
110
+ if (viewport instanceof StackViewport) {
111
+ throw new Error('Stack Viewport Not implemented');
112
+ } else {
113
+ const targetId = this.getTargetId(viewport);
114
+ volumeId = targetId.split(/volumeId:|\?/)[1];
115
+ imageVolume = cache.getVolume(volumeId);
116
+
117
+ referencedImageId = csUtils.getClosestImageId(
118
+ imageVolume,
119
+ worldPos,
120
+ viewPlaneNormal
121
+ );
122
+ }
123
+
124
+ // if (!referencedImageId) {
125
+ // throw new Error('This tool does not work on non-acquisition planes');
126
+ // }
127
+
128
+ const spacingInNormal = csUtils.getSpacingInNormalDirection(
129
+ imageVolume,
130
+ viewPlaneNormal
131
+ );
132
+
133
+ const newStartIndex = this._getStartSliceIndex(
134
+ imageVolume,
135
+ worldPos,
136
+ spacingInNormal,
137
+ viewPlaneNormal
138
+ );
139
+
140
+ // We cannot newStartIndex add numSlicesToPropagate to startIndex because
141
+ // the order of imageIds can be from top to bottom or bottom to top and
142
+ // we want to make sure it is always propagated in the direction of the
143
+ // view and also to make sure we don't go out of bounds.
144
+ const endIndex = this._getEndSliceIndex(
145
+ imageVolume,
146
+ worldPos,
147
+ spacingInNormal,
148
+ viewPlaneNormal
149
+ );
150
+
151
+ const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
152
+
153
+ const annotation = {
154
+ highlighted: true,
155
+ invalidated: true,
156
+ metadata: {
157
+ toolName: this.getToolName(),
158
+ viewPlaneNormal: <Types.Point3>[...viewPlaneNormal],
159
+ viewUp: <Types.Point3>[...viewUp],
160
+ FrameOfReferenceUID,
161
+ referencedImageId,
162
+ volumeId,
163
+ spacingInNormal,
164
+ enabledElement,
165
+ },
166
+ data: {
167
+ label: '',
168
+ startSlice: newStartIndex,
169
+ endSlice: endIndex,
170
+
171
+ handles: {
172
+ textBox: {
173
+ hasMoved: false,
174
+ worldPosition: null,
175
+ worldBoundingBox: null,
176
+ },
177
+ points: [[...worldPos], [...worldPos]] as [
178
+ Types.Point3, // center
179
+ Types.Point3 // end
180
+ ],
181
+ activeHandleIndex: null,
182
+ },
183
+ cachedStats: {
184
+ pointsInVolume: [],
185
+ projectionPoints: [],
186
+ },
187
+ labelmapUID: null,
188
+ },
189
+ };
190
+
191
+ addAnnotation(annotation, element);
192
+
193
+ const viewportIdsToRender = getViewportIdsWithToolToRender(
194
+ element,
195
+ this.getToolName()
196
+ );
197
+
198
+ this.editData = {
199
+ annotation,
200
+ viewportIdsToRender,
201
+ newAnnotation: true,
202
+ hasMoved: false,
203
+ };
204
+
205
+ this._activateDraw(element);
206
+ hideElementCursor(element);
207
+
208
+ evt.preventDefault();
209
+
210
+ triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
211
+
212
+ return annotation;
213
+ };
214
+
215
+ _endCallback = (evt: EventTypes.InteractionEventType): void => {
216
+ const eventDetail = evt.detail;
217
+ const { element } = eventDetail;
218
+
219
+ const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
220
+ this.editData;
221
+ const { data } = annotation;
222
+
223
+ if (newAnnotation && !hasMoved) {
224
+ return;
225
+ }
226
+
227
+ // Circle ROI tool should reset its highlight to false on mouse up (as opposed
228
+ // to other tools that keep it highlighted until the user moves. The reason
229
+ // is that we use top-left and bottom-right handles to define the circle,
230
+ // and they are by definition not in the circle on mouse up.
231
+ annotation.highlighted = false;
232
+ data.handles.activeHandleIndex = null;
233
+
234
+ this._deactivateModify(element);
235
+ this._deactivateDraw(element);
236
+
237
+ resetElementCursor(element);
238
+
239
+ const enabledElement = getEnabledElement(element);
240
+
241
+ this.editData = null;
242
+ this.isDrawing = false;
243
+
244
+ if (
245
+ this.isHandleOutsideImage &&
246
+ this.configuration.preventHandleOutsideImage
247
+ ) {
248
+ removeAnnotation(annotation.annotationUID);
249
+ }
250
+
251
+ const targetId = this.getTargetId(enabledElement.viewport);
252
+ const imageVolume = cache.getVolume(targetId.split(/volumeId:|\?/)[1]);
253
+
254
+ if (this.configuration.calculatePointsInsideVolume) {
255
+ this._computePointsInsideVolume(annotation, imageVolume, enabledElement);
256
+ }
257
+
258
+ triggerAnnotationRenderForViewportIds(
259
+ enabledElement.renderingEngine,
260
+ viewportIdsToRender
261
+ );
262
+
263
+ if (newAnnotation) {
264
+ triggerAnnotationCompleted(annotation);
265
+ }
266
+ };
267
+
268
+ /**
269
+ * it is used to draw the circleROI annotation in each
270
+ * request animation frame. It calculates the updated cached statistics if
271
+ * data is invalidated and cache it.
272
+ *
273
+ * @param enabledElement - The Cornerstone's enabledElement.
274
+ * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
275
+ */
276
+ renderAnnotation = (
277
+ enabledElement: Types.IEnabledElement,
278
+ svgDrawingHelper: SVGDrawingHelper
279
+ ): boolean => {
280
+ let renderStatus = false;
281
+ const { viewport } = enabledElement;
282
+
283
+ const annotations = getAnnotations(this.getToolName(), viewport.element);
284
+
285
+ if (!annotations?.length) {
286
+ return renderStatus;
287
+ }
288
+
289
+ const sliceIndex = viewport.getCurrentImageIdIndex();
290
+
291
+ const styleSpecifier: StyleSpecifier = {
292
+ toolGroupId: this.toolGroupId,
293
+ toolName: this.getToolName(),
294
+ viewportId: enabledElement.viewport.id,
295
+ };
296
+
297
+ for (let i = 0; i < annotations.length; i++) {
298
+ const annotation = annotations[i] as CircleROIStartEndThresholdAnnotation;
299
+ const { annotationUID, data } = annotation;
300
+ const { startSlice, endSlice } = data;
301
+ const { points, activeHandleIndex } = data.handles;
302
+
303
+ styleSpecifier.annotationUID = annotationUID;
304
+
305
+ const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
306
+ const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
307
+ const color = this.getStyle('color', styleSpecifier, annotation);
308
+
309
+ const canvasCoordinates = points.map((p) =>
310
+ viewport.worldToCanvas(p)
311
+ ) as [Types.Point2, Types.Point2];
312
+ const center = canvasCoordinates[0];
313
+
314
+ const radius = getCanvasCircleRadius(canvasCoordinates);
315
+ const { centerPointRadius } = this.configuration;
316
+
317
+ // range of slices to render based on the start and end slice, like
318
+ // np.arange
319
+
320
+ // if indexIJK is outside the start/end slice, we don't render
321
+ if (
322
+ sliceIndex < Math.min(startSlice, endSlice) ||
323
+ sliceIndex > Math.max(startSlice, endSlice)
324
+ ) {
325
+ continue;
326
+ }
327
+
328
+ // WE HAVE TO CACHE STATS BEFORE FETCHING TEXT
329
+
330
+ if (annotation.invalidated) {
331
+ this._throttledCalculateCachedStats(annotation, enabledElement);
332
+ }
333
+
334
+ const middleSlice = Math.round((startSlice + endSlice) / 2);
335
+ // if it is inside the start/end slice, but not exactly the first or
336
+ // last slice, we render the line in dash, but not the handles
337
+
338
+ let isMiddleSlice = false;
339
+ if (sliceIndex === middleSlice) {
340
+ isMiddleSlice = true;
341
+ }
342
+
343
+ // If rendering engine has been destroyed while rendering
344
+ if (!viewport.getRenderingEngine()) {
345
+ console.warn('Rendering Engine has been destroyed');
346
+ return renderStatus;
347
+ }
348
+
349
+ let activeHandleCanvasCoords;
350
+
351
+ if (!isAnnotationVisible(annotationUID)) {
352
+ continue;
353
+ }
354
+
355
+ if (
356
+ !isAnnotationLocked(annotation) &&
357
+ !this.editData &&
358
+ activeHandleIndex !== null &&
359
+ isMiddleSlice
360
+ ) {
361
+ // Not locked or creating and hovering over handle, so render handle.
362
+ activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]];
363
+ }
364
+
365
+ if (activeHandleCanvasCoords) {
366
+ const handleGroupUID = '0';
367
+
368
+ drawHandlesSvg(
369
+ svgDrawingHelper,
370
+ annotationUID,
371
+ handleGroupUID,
372
+ activeHandleCanvasCoords,
373
+ {
374
+ color,
375
+ }
376
+ );
377
+ }
378
+
379
+ let lineWidthToUse = lineWidth;
380
+
381
+ if (isMiddleSlice) {
382
+ lineWidthToUse = 3;
383
+ }
384
+
385
+ const circleUID = '0';
386
+ drawCircleSvg(
387
+ svgDrawingHelper,
388
+ annotationUID,
389
+ circleUID,
390
+ center,
391
+ radius,
392
+ {
393
+ color,
394
+ lineDash,
395
+ lineWidth: lineWidthToUse,
396
+ }
397
+ );
398
+
399
+ // draw center point, if "centerPointRadius" configuration is valid.
400
+ if (centerPointRadius > 0) {
401
+ if (radius > 3 * centerPointRadius) {
402
+ drawCircleSvg(
403
+ svgDrawingHelper,
404
+ annotationUID,
405
+ `${circleUID}-center`,
406
+ center,
407
+ centerPointRadius,
408
+ {
409
+ color,
410
+ lineDash,
411
+ lineWidth,
412
+ }
413
+ );
414
+ }
415
+ }
416
+
417
+ renderStatus = true;
418
+ }
419
+
420
+ return renderStatus;
421
+ };
422
+
423
+ // Todo: make it work for planes other than acquisition planes
424
+ _computeProjectionPoints(
425
+ annotation: CircleROIStartEndThresholdAnnotation,
426
+ imageVolume: Types.IImageVolume
427
+ ): void {
428
+ const { data, metadata } = annotation;
429
+ const { viewPlaneNormal, spacingInNormal } = metadata;
430
+ const { imageData } = imageVolume;
431
+ const { startSlice, endSlice } = data;
432
+ const { points } = data.handles;
433
+
434
+ const startIJK = transformWorldToIndex(imageData, points[0]);
435
+ startIJK[2] = startSlice;
436
+
437
+ if (startIJK[2] !== startSlice) {
438
+ throw new Error('Start slice does not match');
439
+ }
440
+
441
+ // substitute the end slice index 2 with startIJK index 2
442
+ const endIJK = vec3.fromValues(startIJK[0], startIJK[1], endSlice);
443
+
444
+ const startWorld = vec3.create();
445
+ imageData.indexToWorldVec3(startIJK, startWorld);
446
+
447
+ const endWorld = vec3.create();
448
+ imageData.indexToWorldVec3(endIJK, endWorld);
449
+
450
+ // distance between start and end slice in the world coordinate
451
+ const distance = vec3.distance(startWorld, endWorld);
452
+
453
+ // for each point inside points, navigate in the direction of the viewPlaneNormal
454
+ // with amount of spacingInNormal, and calculate the next slice until we reach the distance
455
+ const newProjectionPoints = [];
456
+ for (let dist = 0; dist < distance; dist += spacingInNormal) {
457
+ newProjectionPoints.push(
458
+ points.map((point) => {
459
+ const newPoint = vec3.create();
460
+ //@ts-ignore
461
+ vec3.scaleAndAdd(newPoint, point, viewPlaneNormal, dist);
462
+ return Array.from(newPoint);
463
+ })
464
+ );
465
+ }
466
+
467
+ data.cachedStats.projectionPoints = newProjectionPoints;
468
+ }
469
+
470
+ _computePointsInsideVolume(annotation, imageVolume, enabledElement) {
471
+ const { data } = annotation;
472
+ const { viewport } = enabledElement;
473
+ const projectionPoints = data.cachedStats.projectionPoints;
474
+
475
+ const pointsInsideVolume: Types.Point3[][] = [[]];
476
+
477
+ for (let i = 0; i < projectionPoints.length; i++) {
478
+ // If image does not exists for the targetId, skip. This can be due
479
+ // to various reasons such as if the target was a volumeViewport, and
480
+ // the volumeViewport has been decached in the meantime.
481
+ if (!imageVolume) {
482
+ continue;
483
+ }
484
+
485
+ const centerWorld = projectionPoints[i][0];
486
+ const canvasCoordinates = projectionPoints[i].map((p) =>
487
+ viewport.worldToCanvas(p)
488
+ );
489
+
490
+ const [topLeftCanvas, bottomRightCanvas] = <Array<Types.Point2>>(
491
+ getCanvasCircleCorners(canvasCoordinates)
492
+ );
493
+
494
+ const topLeftWorld = viewport.canvasToWorld(topLeftCanvas);
495
+ const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas);
496
+
497
+ const worldPos1 = topLeftWorld;
498
+ const worldPos2 = bottomRightWorld;
499
+
500
+ const { dimensions, imageData } = imageVolume;
501
+
502
+ const worldPos1Index = transformWorldToIndex(imageData, worldPos1);
503
+ const worldCenterIndex = transformWorldToIndex(imageData, centerWorld);
504
+
505
+ worldPos1Index[0] = Math.floor(worldPos1Index[0]);
506
+ worldPos1Index[1] = Math.floor(worldPos1Index[1]);
507
+ worldPos1Index[2] = Math.floor(worldCenterIndex[2]);
508
+
509
+ const worldPos2Index = transformWorldToIndex(imageData, worldPos2);
510
+
511
+ worldPos2Index[0] = Math.floor(worldPos2Index[0]);
512
+ worldPos2Index[1] = Math.floor(worldPos2Index[1]);
513
+ worldPos2Index[2] = Math.floor(worldCenterIndex[2]);
514
+
515
+ // Check if one of the indexes are inside the volume, this then gives us
516
+ // Some area to do stats over.
517
+
518
+ if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) {
519
+ const iMin = Math.min(worldPos1Index[0], worldPos2Index[0]);
520
+ const iMax = Math.max(worldPos1Index[0], worldPos2Index[0]);
521
+
522
+ const jMin = Math.min(worldPos1Index[1], worldPos2Index[1]);
523
+ const jMax = Math.max(worldPos1Index[1], worldPos2Index[1]);
524
+
525
+ const kMin = Math.min(worldPos1Index[2], worldPos2Index[2]);
526
+ const kMax = Math.max(worldPos1Index[2], worldPos2Index[2]);
527
+
528
+ const boundsIJK = [
529
+ [iMin, iMax],
530
+ [jMin, jMax],
531
+ [kMin, kMax],
532
+ ] as [Types.Point2, Types.Point2, Types.Point2];
533
+
534
+ const center = centerWorld as Types.Point3;
535
+
536
+ const ellipseObj = {
537
+ center,
538
+ xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2,
539
+ yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2,
540
+ zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2,
541
+ };
542
+
543
+ const pointsInShape = pointInShapeCallback(
544
+ imageData,
545
+ //@ts-ignore
546
+ (pointLPS) => pointInEllipse(ellipseObj, pointLPS),
547
+ null,
548
+ boundsIJK
549
+ );
550
+
551
+ //@ts-ignore
552
+ pointsInsideVolume.push(pointsInShape);
553
+ }
554
+ }
555
+ data.cachedStats.pointsInVolume = pointsInsideVolume;
556
+ }
557
+
558
+ _calculateCachedStatsTool(annotation, enabledElement) {
559
+ const data = annotation.data;
560
+ const { viewportId, renderingEngineId, viewport } = enabledElement;
561
+
562
+ const { cachedStats } = data;
563
+ const targetId = this.getTargetId(viewport);
564
+ const imageVolume = cache.getVolume(targetId.split(/volumeId:|\?/)[1]);
565
+
566
+ // Todo: this shouldn't be here, this is a performance issue
567
+ // Since we are extending the RectangleROI class, we need to
568
+ // bring the logic for handle to some cachedStats calculation
569
+ this._computeProjectionPoints(annotation, imageVolume);
570
+
571
+ annotation.invalidated = false;
572
+
573
+ // Dispatching annotation modified
574
+ const eventType = Events.ANNOTATION_MODIFIED;
575
+
576
+ const eventDetail: AnnotationModifiedEventDetail = {
577
+ annotation,
578
+ viewportId,
579
+ renderingEngineId,
580
+ };
581
+ triggerEvent(eventTarget, eventType, eventDetail);
582
+
583
+ return cachedStats;
584
+ }
585
+
586
+ _getStartSliceIndex(
587
+ imageVolume: Types.IImageVolume,
588
+ worldPos: Types.Point3,
589
+ spacingInNormal: number,
590
+ viewPlaneNormal: Types.Point3
591
+ ): number | undefined {
592
+ const numSlicesToPropagate = this.configuration.numSlicesToPropagate;
593
+
594
+ const numSlicesToPropagateFromStart = Math.round(numSlicesToPropagate / 2);
595
+ // get end position by moving from worldPos in the direction of viewplaneNormal
596
+ // with amount of numSlicesToPropagate * spacingInNormal
597
+ const startPos = vec3.create();
598
+ vec3.scaleAndAdd(
599
+ startPos,
600
+ worldPos,
601
+ viewPlaneNormal,
602
+ numSlicesToPropagateFromStart * -spacingInNormal
603
+ );
604
+
605
+ const imageIdIndex = this._getImageIdIndex(
606
+ imageVolume,
607
+ startPos,
608
+ spacingInNormal,
609
+ viewPlaneNormal
610
+ );
611
+
612
+ return imageIdIndex;
613
+ }
614
+
615
+ _getEndSliceIndex(
616
+ imageVolume: Types.IImageVolume,
617
+ worldPos: Types.Point3,
618
+ spacingInNormal: number,
619
+ viewPlaneNormal: Types.Point3
620
+ ): number | undefined {
621
+ const numSlicesToPropagate = this.configuration.numSlicesToPropagate;
622
+ const numSlicesToPropagateFromStart = Math.round(numSlicesToPropagate / 2);
623
+
624
+ // get end position by moving from worldPos in the direction of viewplaneNormal
625
+ // with amount of numSlicesToPropagate * spacingInNormal
626
+ const endPos = vec3.create();
627
+ vec3.scaleAndAdd(
628
+ endPos,
629
+ worldPos,
630
+ viewPlaneNormal,
631
+ numSlicesToPropagateFromStart * spacingInNormal
632
+ );
633
+
634
+ const imageIdIndex = this._getImageIdIndex(
635
+ imageVolume,
636
+ endPos,
637
+ spacingInNormal,
638
+ viewPlaneNormal
639
+ );
640
+
641
+ return imageIdIndex;
642
+ }
643
+
644
+ _getImageIdIndex(
645
+ imageVolume: Types.IImageVolume,
646
+ pos: vec3,
647
+ spacingInNormal: number,
648
+ viewPlaneNormal: Types.Point3
649
+ ): number | undefined {
650
+ const halfSpacingInNormalDirection = spacingInNormal / 2;
651
+ // Loop through imageIds of the imageVolume and find the one that is closest to endPos
652
+ const { imageIds } = imageVolume;
653
+ let imageIdIndex;
654
+ for (let i = 0; i < imageIds.length; i++) {
655
+ const imageId = imageIds[i];
656
+
657
+ const { imagePositionPatient } = metaData.get(
658
+ 'imagePlaneModule',
659
+ imageId
660
+ );
661
+
662
+ const dir = vec3.create();
663
+ vec3.sub(dir, pos, imagePositionPatient);
664
+
665
+ const dot = vec3.dot(dir, viewPlaneNormal);
666
+
667
+ if (Math.abs(dot) < halfSpacingInNormalDirection) {
668
+ imageIdIndex = i;
669
+ }
670
+ }
671
+
672
+ return imageIdIndex;
673
+ }
674
+ }
675
+
676
+ CircleROIStartEndThresholdTool.toolName = 'CircleROIStartEndThreshold';
677
+ export default CircleROIStartEndThresholdTool;