@cornerstonejs/tools 4.12.3 → 4.12.5

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 (32) hide show
  1. package/dist/esm/enums/MeasurementType.d.ts +6 -0
  2. package/dist/esm/enums/MeasurementType.js +7 -0
  3. package/dist/esm/enums/index.d.ts +1 -0
  4. package/dist/esm/enums/index.js +1 -0
  5. package/dist/esm/tools/SculptorTool/CircleSculptCursor.d.ts +8 -10
  6. package/dist/esm/tools/SculptorTool/CircleSculptCursor.js +33 -133
  7. package/dist/esm/tools/SculptorTool.d.ts +20 -5
  8. package/dist/esm/tools/SculptorTool.js +243 -52
  9. package/dist/esm/tools/annotation/BidirectionalTool.d.ts +0 -2
  10. package/dist/esm/tools/annotation/BidirectionalTool.js +24 -28
  11. package/dist/esm/tools/annotation/CircleROITool.d.ts +1 -2
  12. package/dist/esm/tools/annotation/CircleROITool.js +51 -44
  13. package/dist/esm/tools/annotation/EllipticalROITool.js +1 -1
  14. package/dist/esm/tools/annotation/LengthTool.d.ts +0 -2
  15. package/dist/esm/tools/annotation/LengthTool.js +13 -25
  16. package/dist/esm/tools/annotation/PlanarFreehandROITool.d.ts +2 -1
  17. package/dist/esm/tools/annotation/PlanarFreehandROITool.js +70 -68
  18. package/dist/esm/tools/base/BaseTool.d.ts +4 -2
  19. package/dist/esm/tools/base/BaseTool.js +38 -11
  20. package/dist/esm/tools/segmentation/BrushTool.js +9 -0
  21. package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +4 -1
  22. package/dist/esm/types/CalculatorTypes.d.ts +4 -3
  23. package/dist/esm/types/ISculptToolShape.d.ts +6 -4
  24. package/dist/esm/utilities/contours/index.d.ts +1 -2
  25. package/dist/esm/utilities/contours/index.js +1 -2
  26. package/dist/esm/utilities/getCalibratedUnits.d.ts +2 -0
  27. package/dist/esm/utilities/getCalibratedUnits.js +32 -63
  28. package/dist/esm/version.d.ts +1 -1
  29. package/dist/esm/version.js +1 -1
  30. package/package.json +3 -3
  31. package/dist/esm/utilities/contours/calculatePerimeter.d.ts +0 -2
  32. package/dist/esm/utilities/contours/calculatePerimeter.js +0 -16
@@ -1,6 +1,7 @@
1
- import { getEnabledElement } from '@cornerstonejs/core';
1
+ import { getEnabledElement, utilities } from '@cornerstonejs/core';
2
+ import { vec3 } from 'gl-matrix';
2
3
  import { BaseTool } from './base';
3
- import { getAnnotations } from '../stateManagement';
4
+ import { getAnnotations, getAnnotation } from '../stateManagement';
4
5
  import { point } from '../utilities/math';
5
6
  import { Events, ToolModes, AnnotationStyleStates, ChangeTypes, } from '../enums';
6
7
  import { triggerAnnotationRenderForViewportIds } from '../utilities/triggerAnnotationRenderForViewportIds';
@@ -10,6 +11,8 @@ import { triggerAnnotationModified } from '../stateManagement/annotation/helpers
10
11
  import CircleSculptCursor from './SculptorTool/CircleSculptCursor';
11
12
  import { distancePointToContour } from './distancePointToContour';
12
13
  import { getToolGroupForViewport } from '../store/ToolGroupManager';
14
+ import { getSignedArea, containsPoint } from '../utilities/math/polyline';
15
+ const { isEqual } = utilities;
13
16
  class SculptorTool extends BaseTool {
14
17
  constructor(toolProps = {}, defaultToolProps = {
15
18
  supportedInteractionTypes: ['Mouse', 'Touch'],
@@ -21,7 +24,6 @@ class SculptorTool extends BaseTool {
21
24
  ],
22
25
  toolShape: 'circle',
23
26
  referencedToolName: 'PlanarFreehandROI',
24
- updateCursorSize: 'dynamic',
25
27
  },
26
28
  }) {
27
29
  super(toolProps, defaultToolProps);
@@ -32,10 +34,12 @@ class SculptorTool extends BaseTool {
32
34
  viewportIdsToRender: [],
33
35
  isEditingOpenContour: false,
34
36
  canvasLocation: undefined,
37
+ external: true,
38
+ closed: false,
35
39
  };
36
40
  this.preMouseDownCallback = (evt) => {
37
41
  const eventData = evt.detail;
38
- const element = eventData.element;
42
+ const { element } = eventData;
39
43
  this.configureToolSize(evt);
40
44
  this.selectFreehandTool(eventData);
41
45
  if (this.commonData.activeAnnotationUID === null) {
@@ -58,14 +62,11 @@ class SculptorTool extends BaseTool {
58
62
  this.endCallback = (evt) => {
59
63
  const eventData = evt.detail;
60
64
  const { element } = eventData;
61
- const config = this.configuration;
62
- const enabledElement = getEnabledElement(element);
63
65
  this.isActive = false;
64
66
  this.deactivateModify(element);
65
67
  resetElementCursor(element);
66
- const { renderingEngineId, viewportId } = enabledElement;
67
- const toolGroup = getToolGroupForViewport(viewportId, renderingEngineId);
68
- const toolInstance = toolGroup.getToolInstance(config.referencedToolName);
68
+ const toolInstance = this.getToolInstance(element);
69
+ toolInstance.doneEditMemo?.();
69
70
  const annotations = this.filterSculptableAnnotationsForElement(element);
70
71
  const activeAnnotation = annotations.find((annotation) => annotation.annotationUID === this.commonData.activeAnnotationUID);
71
72
  if (toolInstance.configuration.calculateStats) {
@@ -101,15 +102,209 @@ class SculptorTool extends BaseTool {
101
102
  this.sculptData = {
102
103
  mousePoint: eventData.currentPoints.world,
103
104
  mouseCanvasPoint: eventData.currentPoints.canvas,
104
- deltaWorld: eventData.deltaPoints.world,
105
105
  points,
106
106
  maxSpacing: cursorShape.getMaxSpacing(config.minSpacing),
107
107
  element: element,
108
+ contours: [
109
+ {
110
+ annotationUID: this.commonData.activeAnnotationUID,
111
+ points,
112
+ },
113
+ ],
108
114
  };
109
- const pushedHandles = cursorShape.pushHandles(viewport, this.sculptData);
110
- if (pushedHandles.first !== undefined) {
111
- this.insertNewHandles(pushedHandles);
115
+ const intersections = this.intersect(viewport, cursorShape);
116
+ if (!intersections.length) {
117
+ return;
118
+ }
119
+ const contourSelections = this.getContourSelections(intersections, points.length);
120
+ const { closed } = this.commonData;
121
+ for (const contour of contourSelections) {
122
+ const newPoints = new Array();
123
+ const lastExit = contour[contour.length - 1];
124
+ let lastIndex = closed ? lastExit.relIndex : 0;
125
+ let lastEnter;
126
+ for (const intersection of contour) {
127
+ if (intersection.isEnter) {
128
+ pushArr(newPoints, points, lastIndex, intersection.index);
129
+ lastEnter = intersection;
130
+ }
131
+ else {
132
+ this.interpolatePoints(viewport, lastEnter, intersection, points, newPoints);
133
+ }
134
+ lastIndex = intersection.index;
135
+ }
136
+ if (contourSelections.length > 1) {
137
+ const signedArea = getSignedArea(newPoints.map(viewport.worldToCanvas));
138
+ if (signedArea < 0) {
139
+ console.warn('Skipping internal area');
140
+ continue;
141
+ }
142
+ }
143
+ if (!closed && lastIndex < points.length - 1) {
144
+ pushArr(newPoints, points, lastIndex);
145
+ }
146
+ points.splice(0, points.length);
147
+ pushArr(points, newPoints);
148
+ return;
149
+ }
150
+ }
151
+ intersect(viewport, cursorShape) {
152
+ const { contours, mousePoint, mouseCanvasPoint } = this.sculptData;
153
+ const { closed } = this.commonData;
154
+ cursorShape.computeWorldRadius(viewport);
155
+ const result = new Array();
156
+ for (const contour of contours) {
157
+ const { annotationUID, points } = contour;
158
+ let lastIn = false;
159
+ let anyIn = false;
160
+ let anyOut = false;
161
+ const { length } = points;
162
+ for (let i = 0; i <= length; i++) {
163
+ const index = i % length;
164
+ const point = points[index];
165
+ const inCursor = cursorShape.isInCursor(point, mousePoint);
166
+ anyIn ||= inCursor;
167
+ anyOut ||= !inCursor;
168
+ if (i === 0) {
169
+ lastIn = inCursor;
170
+ if (!closed && inCursor) {
171
+ const edge = cursorShape.getEdge(viewport, point, null, mouseCanvasPoint);
172
+ result.push({
173
+ annotationUID,
174
+ isEnter: inCursor,
175
+ index: i,
176
+ point: edge.point,
177
+ angle: edge.angle,
178
+ });
179
+ }
180
+ continue;
181
+ }
182
+ if (index === 0 && !closed) {
183
+ if (lastIn) {
184
+ const edge = cursorShape.getEdge(viewport, points[length - 1], null, mouseCanvasPoint);
185
+ result.push({
186
+ annotationUID,
187
+ isEnter: false,
188
+ index: length - 1,
189
+ point: edge.point,
190
+ angle: edge.angle,
191
+ });
192
+ }
193
+ continue;
194
+ }
195
+ if (lastIn === inCursor) {
196
+ continue;
197
+ }
198
+ lastIn = inCursor;
199
+ const edge = cursorShape.getEdge(viewport, point, points[i - 1], mouseCanvasPoint);
200
+ result.push({
201
+ annotationUID,
202
+ isEnter: inCursor,
203
+ index: i,
204
+ point: edge.point,
205
+ angle: edge.angle,
206
+ });
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+ interpolatePoints(viewport, enter, exit, existing, newPoints) {
212
+ const { external, closed } = this.commonData;
213
+ const p0 = existing[enter.index % existing.length];
214
+ const p1 = existing[exit.index % existing.length];
215
+ const v = vec3.sub(vec3.create(), p1, p0);
216
+ if (isEqual(vec3.length(v), 0)) {
217
+ return;
218
+ }
219
+ const cursorShape = this.registeredShapes.get(this.selectedShape);
220
+ const a0 = (enter.angle + 2 * Math.PI) % (Math.PI * 2);
221
+ const a1 = (exit.angle + 2 * Math.PI) % (Math.PI * 2);
222
+ let ae = a1 < a0 ? a1 + 2 * Math.PI : a1;
223
+ const aeAlt = a1 > a0 ? a1 - 2 * Math.PI : a1;
224
+ if ((external && !closed && Math.abs(aeAlt - a0) < Math.abs(ae - a0)) ||
225
+ (external && closed)) {
226
+ ae = aeAlt;
227
+ }
228
+ const count = Math.ceil(Math.abs(a0 - ae) / 0.25);
229
+ for (let i = 0; i <= count; i++) {
230
+ const a = (a0 * (count - i) + i * ae) / count;
231
+ newPoints.push(cursorShape.interpolatePoint(viewport, a, this.sculptData.mouseCanvasPoint));
232
+ }
233
+ }
234
+ getContourSelections(intersections, pointLength) {
235
+ const result = new Array();
236
+ const enterLength = intersections.length / 2;
237
+ if (!enterLength || intersections.length % 2) {
238
+ return result;
239
+ }
240
+ let lastAngle = Number.NEGATIVE_INFINITY;
241
+ for (let enterCount = 0; enterCount < enterLength; enterCount++) {
242
+ const enter = this.findNext(intersections, lastAngle);
243
+ if (!enter) {
244
+ console.error("Couldnt' find an entry");
245
+ continue;
246
+ }
247
+ const exit = this.findNext(intersections, enter.angle, false);
248
+ if (!exit) {
249
+ console.error("Couldn't find an exit for", enter);
250
+ continue;
251
+ }
252
+ exit.relIndex ||=
253
+ exit.index < enter.index ? exit.index + pointLength : exit.index;
254
+ result.push([enter, exit]);
255
+ }
256
+ result.sort((a, b) => a[0].index - b[0].index);
257
+ for (let i = 0; i < result.length - 1;) {
258
+ const testIntersection = result[i];
259
+ const mergeableResult = this.findMergeable(result, testIntersection, i);
260
+ if (mergeableResult) {
261
+ testIntersection.push(...mergeableResult);
262
+ }
263
+ else {
264
+ i++;
265
+ }
266
+ }
267
+ if (result.length > 1) {
268
+ console.warn('************* More than 1 result', result);
112
269
  }
270
+ return result;
271
+ }
272
+ findMergeable(contours, testIntersection, currentIndex) {
273
+ const end = testIntersection[testIntersection.length - 1];
274
+ for (let i = currentIndex + 1; i < contours.length; i++) {
275
+ const [enter] = contours[i];
276
+ if (enter.index >= end.relIndex) {
277
+ const contour = contours[i];
278
+ contours.splice(i, 1);
279
+ return contour;
280
+ }
281
+ }
282
+ }
283
+ findNext(intersections, lastAngle, isEnter = true) {
284
+ if (intersections.length === 1) {
285
+ const [intersection] = intersections;
286
+ intersections.splice(0, 1);
287
+ return intersection;
288
+ }
289
+ let foundItem;
290
+ let testAngle;
291
+ for (let i = 0; i < intersections.length; i++) {
292
+ const intersection = intersections[i];
293
+ if (intersection.isEnter == isEnter) {
294
+ const relativeAngle = (intersection.angle - lastAngle + 2 * Math.PI) % (2 * Math.PI);
295
+ if (!foundItem || relativeAngle < testAngle) {
296
+ foundItem = { i, intersection };
297
+ testAngle = relativeAngle;
298
+ }
299
+ }
300
+ }
301
+ if (!foundItem) {
302
+ console.warn("Couldn't find an exit point for entry", JSON.stringify(intersections));
303
+ return;
304
+ }
305
+ intersections.splice(foundItem.i, 1);
306
+ const { intersection } = foundItem;
307
+ return intersection;
113
308
  }
114
309
  interpolatePointsWithinMaxSpacing(i, points, indicesToInsertAfter, maxSpacing) {
115
310
  const { element } = this.sculptData;
@@ -127,7 +322,7 @@ class SculptorTool extends BaseTool {
127
322
  const eventData = evt.detail;
128
323
  const element = eventData.element;
129
324
  const enabledElement = getEnabledElement(element);
130
- const { renderingEngine, viewport } = enabledElement;
325
+ const { viewport } = enabledElement;
131
326
  this.commonData.viewportIdsToRender = [viewport.id];
132
327
  const annotations = this.filterSculptableAnnotationsForElement(element);
133
328
  if (!annotations?.length) {
@@ -141,20 +336,22 @@ class SculptorTool extends BaseTool {
141
336
  else {
142
337
  const cursorShape = this.registeredShapes.get(this.selectedShape);
143
338
  const canvasCoords = eventData.currentPoints.canvas;
144
- if (this.configuration.updateCursorSize === 'dynamic') {
145
- cursorShape.updateToolSize(canvasCoords, viewport, activeAnnotation);
146
- }
339
+ cursorShape.updateToolSize(canvasCoords, viewport, activeAnnotation);
147
340
  }
148
341
  triggerAnnotationRenderForViewportIds(this.commonData.viewportIdsToRender);
149
342
  }
150
- filterSculptableAnnotationsForElement(element) {
151
- const config = this.configuration;
343
+ getToolInstance(element) {
152
344
  const enabledElement = getEnabledElement(element);
153
345
  const { renderingEngineId, viewportId } = enabledElement;
154
- const sculptableAnnotations = [];
155
346
  const toolGroup = getToolGroupForViewport(viewportId, renderingEngineId);
156
- const toolInstance = toolGroup.getToolInstance(config.referencedToolName);
157
- config.referencedToolNames.forEach((referencedToolName) => {
347
+ const toolInstance = toolGroup.getToolInstance(this.configuration.referencedToolName);
348
+ return toolInstance;
349
+ }
350
+ filterSculptableAnnotationsForElement(element) {
351
+ const { configuration } = this;
352
+ const sculptableAnnotations = [];
353
+ const toolInstance = this.getToolInstance(element);
354
+ configuration.referencedToolNames.forEach((referencedToolName) => {
158
355
  const annotations = getAnnotations(referencedToolName, element);
159
356
  if (annotations) {
160
357
  sculptableAnnotations.push(...annotations);
@@ -166,42 +363,25 @@ class SculptorTool extends BaseTool {
166
363
  const cursorShape = this.registeredShapes.get(this.selectedShape);
167
364
  cursorShape.configureToolSize(evt);
168
365
  }
169
- insertNewHandles(pushedHandles) {
170
- const indicesToInsertAfter = this.findNewHandleIndices(pushedHandles);
171
- let newIndexModifier = 0;
172
- for (let i = 0; i < indicesToInsertAfter?.length; i++) {
173
- const insertIndex = indicesToInsertAfter[i] + 1 + newIndexModifier;
174
- this.insertHandleRadially(insertIndex);
175
- newIndexModifier++;
176
- }
177
- }
178
- findNewHandleIndices(pushedHandles) {
179
- const { points, maxSpacing } = this.sculptData;
180
- const indicesToInsertAfter = [];
181
- for (let i = pushedHandles.first; i <= pushedHandles.last; i++) {
182
- this.interpolatePointsWithinMaxSpacing(i, points, indicesToInsertAfter, maxSpacing);
183
- }
184
- return indicesToInsertAfter;
185
- }
186
- insertHandleRadially(insertIndex) {
187
- const { points } = this.sculptData;
188
- if (insertIndex > points.length - 1 &&
189
- this.commonData.isEditingOpenContour) {
190
- return;
191
- }
192
- const cursorShape = this.registeredShapes.get(this.selectedShape);
193
- const previousIndex = insertIndex - 1;
194
- const nextIndex = contourIndex(insertIndex, points.length);
195
- const insertPosition = cursorShape.getInsertPosition(previousIndex, nextIndex, this.sculptData);
196
- const handleData = insertPosition;
197
- points.splice(insertIndex, 0, handleData);
198
- }
199
366
  selectFreehandTool(eventData) {
200
367
  const closestAnnotationUID = this.getClosestFreehandToolOnElement(eventData);
201
368
  if (closestAnnotationUID === undefined) {
202
369
  return;
203
370
  }
371
+ const annotation = getAnnotation(closestAnnotationUID);
204
372
  this.commonData.activeAnnotationUID = closestAnnotationUID;
373
+ this.commonData.closed = annotation.data.contour.closed;
374
+ this.commonData.external = true;
375
+ if (this.commonData.closed) {
376
+ const { element } = eventData;
377
+ const enabledElement = getEnabledElement(element);
378
+ const { viewport } = enabledElement;
379
+ const polyline = annotation.data.contour.polyline.map((p) => viewport.worldToCanvas(p));
380
+ const canvasPoint = eventData.currentPoints.canvas;
381
+ this.commonData.external = !containsPoint(polyline, canvasPoint, {
382
+ closed: true,
383
+ });
384
+ }
205
385
  }
206
386
  getClosestFreehandToolOnElement(eventData) {
207
387
  const { element } = eventData;
@@ -239,6 +419,9 @@ class SculptorTool extends BaseTool {
239
419
  return closest.annotationUID;
240
420
  }
241
421
  activateModify(element) {
422
+ const annotation = getAnnotation(this.commonData.activeAnnotationUID);
423
+ const instance = this.getToolInstance(element);
424
+ instance.createMemo?.(element, annotation);
242
425
  element.addEventListener(Events.MOUSE_UP, this.endCallback);
243
426
  element.addEventListener(Events.MOUSE_CLICK, this.endCallback);
244
427
  element.addEventListener(Events.MOUSE_DRAG, this.dragCallback);
@@ -286,6 +469,14 @@ class SculptorTool extends BaseTool {
286
469
  });
287
470
  }
288
471
  }
472
+ function pushArr(dest, src, start = 0, end = src.length) {
473
+ if (end < start) {
474
+ end = end + src.length;
475
+ }
476
+ for (let i = start; i < end; i++) {
477
+ dest.push(src[i % src.length]);
478
+ }
479
+ }
289
480
  export const contourIndex = (i, length) => {
290
481
  return (i + length) % length;
291
482
  };
@@ -39,9 +39,7 @@ declare class BidirectionalTool extends AnnotationTool {
39
39
  _deactivateModify: (element: any) => void;
40
40
  renderAnnotation: (enabledElement: Types.IEnabledElement, svgDrawingHelper: SVGDrawingHelper) => boolean;
41
41
  _movingLongAxisWouldPutItThroughShortAxis: (firstLineSegment: any, secondLineSegment: any) => boolean;
42
- _calculateLength(pos1: any, pos2: any): number;
43
42
  _calculateCachedStats: (annotation: any, renderingEngine: any, enabledElement: any) => any;
44
- _isInsideVolume: (index1: any, index2: any, index3: any, index4: any, dimensions: any) => boolean;
45
43
  _getSignedAngle: (vector1: any, vector2: any) => number;
46
44
  }
47
45
  export default BidirectionalTool;
@@ -9,7 +9,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
9
9
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
10
10
  import { drawLine as drawLineSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
11
11
  import { state } from '../../store/state';
12
- import { ChangeTypes, Events } from '../../enums';
12
+ import { ChangeTypes, Events, MeasurementType } from '../../enums';
13
13
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
14
14
  import * as lineSegment from '../../utilities/math/line';
15
15
  import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
@@ -610,28 +610,36 @@ class BidirectionalTool extends AnnotationTool {
610
610
  continue;
611
611
  }
612
612
  const { imageData, dimensions } = image;
613
- const index1 = transformWorldToIndex(imageData, worldPos1);
614
- const index2 = transformWorldToIndex(imageData, worldPos2);
615
- const index3 = transformWorldToIndex(imageData, worldPos3);
616
- const index4 = transformWorldToIndex(imageData, worldPos4);
617
- const handles1 = [index1, index2];
618
- const handles2 = [index3, index4];
619
- const { scale: scale1, unit: units1 } = getCalibratedLengthUnitsAndScale(image, handles1);
620
- const { scale: scale2, unit: units2 } = getCalibratedLengthUnitsAndScale(image, handles2);
621
- const dist1 = this._calculateLength(worldPos1, worldPos2) / scale1;
622
- const dist2 = this._calculateLength(worldPos3, worldPos4) / scale2;
613
+ const handles = data.handles.points.map((point) => imageData.worldToIndex(point));
614
+ const handles1 = handles.slice(0, 2);
615
+ const handles2 = handles.slice(2, 4);
616
+ const calibrate = getCalibratedLengthUnitsAndScale(image, handles);
617
+ const dist1 = BidirectionalTool.calculateLengthInIndex(calibrate, handles1);
618
+ const dist2 = BidirectionalTool.calculateLengthInIndex(calibrate, handles2);
619
+ const { unit } = calibrate;
623
620
  const length = dist1 > dist2 ? dist1 : dist2;
624
621
  const width = dist1 > dist2 ? dist2 : dist1;
625
- const unit = dist1 > dist2 ? units1 : units2;
626
- const widthUnit = dist1 > dist2 ? units2 : units1;
627
- this._isInsideVolume(index1, index2, index3, index4, dimensions)
628
- ? (this.isHandleOutsideImage = false)
629
- : (this.isHandleOutsideImage = true);
622
+ const widthUnit = unit;
623
+ this.isHandleOutsideImage = !BidirectionalTool.isInsideVolume(dimensions, handles);
630
624
  cachedStats[targetId] = {
631
625
  length,
632
626
  width,
633
627
  unit,
634
628
  widthUnit,
629
+ statsArray: [
630
+ {
631
+ value: length,
632
+ name: 'height',
633
+ unit,
634
+ type: MeasurementType.Linear,
635
+ },
636
+ {
637
+ value: width,
638
+ name: 'width',
639
+ unit,
640
+ type: MeasurementType.Linear,
641
+ },
642
+ ],
635
643
  };
636
644
  }
637
645
  const invalidated = annotation.invalidated;
@@ -641,12 +649,6 @@ class BidirectionalTool extends AnnotationTool {
641
649
  }
642
650
  return cachedStats;
643
651
  };
644
- this._isInsideVolume = (index1, index2, index3, index4, dimensions) => {
645
- return (csUtils.indexWithinDimensions(index1, dimensions) &&
646
- csUtils.indexWithinDimensions(index2, dimensions) &&
647
- csUtils.indexWithinDimensions(index3, dimensions) &&
648
- csUtils.indexWithinDimensions(index4, dimensions));
649
- };
650
652
  this._getSignedAngle = (vector1, vector2) => {
651
653
  return Math.atan2(vector1[0] * vector2[1] - vector1[1] * vector2[0], vector1[0] * vector2[0] + vector1[1] * vector2[1]);
652
654
  };
@@ -726,12 +728,6 @@ class BidirectionalTool extends AnnotationTool {
726
728
  triggerAnnotationRenderForViewportIds([viewport.id]);
727
729
  return annotation;
728
730
  }; }
729
- _calculateLength(pos1, pos2) {
730
- const dx = pos1[0] - pos2[0];
731
- const dy = pos1[1] - pos2[1];
732
- const dz = pos1[2] - pos2[2];
733
- return Math.sqrt(dx * dx + dy * dy + dz * dz);
734
- }
735
731
  }
736
732
  function defaultGetTextLines(data, targetId) {
737
733
  const { cachedStats, label } = data;
@@ -30,8 +30,7 @@ declare class CircleROITool extends AnnotationTool {
30
30
  _activateDraw: (element: any) => void;
31
31
  _deactivateDraw: (element: any) => void;
32
32
  renderAnnotation: (enabledElement: Types.IEnabledElement, svgDrawingHelper: SVGDrawingHelper) => boolean;
33
- _calculateCachedStats: (annotation: any, viewport: any, renderingEngine: any, enabledElement: any) => any;
34
- _isInsideVolume: (index1: any, index2: any, dimensions: any) => boolean;
33
+ _calculateCachedStats: (annotation: any, viewport: any, _renderingEngine: any, _enabledElement: any) => any;
35
34
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
36
35
  annotationUID?: string;
37
36
  toolInstance?: CircleROITool;
@@ -1,6 +1,7 @@
1
- import { AnnotationTool } from '../base';
1
+ import { AnnotationTool, BaseTool } from '../base';
2
+ import { vec2, vec3 } from 'gl-matrix';
2
3
  import { getEnabledElement, VolumeViewport, utilities as csUtils, getEnabledElementByViewportId, EPSILON, } from '@cornerstonejs/core';
3
- import { getCalibratedAspect, getCalibratedLengthUnitsAndScale, } from '../../utilities/getCalibratedUnits';
4
+ import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits';
4
5
  import throttle from '../../utilities/throttle';
5
6
  import { addAnnotation, getAnnotations, removeAnnotation, } from '../../stateManagement/annotation/annotationState';
6
7
  import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
@@ -8,10 +9,9 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
8
9
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
9
10
  import { drawCircle as drawCircleSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
10
11
  import { state } from '../../store/state';
11
- import { ChangeTypes, Events } from '../../enums';
12
+ import { ChangeTypes, Events, MeasurementType } from '../../enums';
12
13
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
13
14
  import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
14
- import getWorldWidthAndHeightFromTwoPoints from '../../utilities/planar/getWorldWidthAndHeightFromTwoPoints';
15
15
  import { resetElementCursor, hideElementCursor, } from '../../cursors/elementCursor';
16
16
  import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
17
17
  import { getPixelValueUnits } from '../../utilities/getPixelValueUnits';
@@ -19,7 +19,6 @@ import { isViewportPreScaled } from '../../utilities/viewport/isViewportPreScale
19
19
  import { getCanvasCircleCorners, getCanvasCircleRadius, } from '../../utilities/math/circle';
20
20
  import { pointInEllipse } from '../../utilities/math/ellipse';
21
21
  import { BasicStatsCalculator } from '../../utilities/math/basic';
22
- import { vec2, vec3 } from 'gl-matrix';
23
22
  import { getStyleProperty } from '../../stateManagement/annotation/config/helpers';
24
23
  const { transformWorldToIndex } = csUtils;
25
24
  class CircleROITool extends AnnotationTool {
@@ -473,25 +472,22 @@ class CircleROITool extends AnnotationTool {
473
472
  }
474
473
  return renderStatus;
475
474
  };
476
- this._calculateCachedStats = (annotation, viewport, renderingEngine, enabledElement) => {
475
+ this._calculateCachedStats = (annotation, viewport, _renderingEngine, _enabledElement) => {
477
476
  if (!this.configuration.calculateStats) {
478
477
  return;
479
478
  }
480
- const data = annotation.data;
479
+ const { data } = annotation;
481
480
  const { element } = viewport;
482
481
  const wasInvalidated = annotation.invalidated;
483
482
  const { points } = data.handles;
484
483
  const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
485
484
  const canvasCenter = canvasCoordinates[0];
486
485
  const canvasTop = canvasCoordinates[1];
487
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
488
486
  const [topLeftCanvas, bottomRightCanvas] = (getCanvasCircleCorners([canvasCenter, canvasTop]));
489
487
  const topLeftWorld = viewport.canvasToWorld(topLeftCanvas);
490
488
  const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas);
491
489
  const { cachedStats } = data;
492
490
  const targetIds = Object.keys(cachedStats);
493
- const worldPos1 = topLeftWorld;
494
- const worldPos2 = bottomRightWorld;
495
491
  for (let i = 0; i < targetIds.length; i++) {
496
492
  const targetId = targetIds[i];
497
493
  const image = this.getTargetImageData(targetId);
@@ -499,15 +495,49 @@ class CircleROITool extends AnnotationTool {
499
495
  continue;
500
496
  }
501
497
  const { dimensions, imageData, metadata, voxelManager } = image;
502
- const pos1Index = transformWorldToIndex(imageData, worldPos1);
503
- pos1Index[0] = Math.floor(pos1Index[0]);
504
- pos1Index[1] = Math.floor(pos1Index[1]);
505
- pos1Index[2] = Math.floor(pos1Index[2]);
506
- const pos2Index = transformWorldToIndex(imageData, worldPos2);
507
- pos2Index[0] = Math.floor(pos2Index[0]);
508
- pos2Index[1] = Math.floor(pos2Index[1]);
509
- pos2Index[2] = Math.floor(pos2Index[2]);
510
- if (this._isInsideVolume(pos1Index, pos2Index, dimensions)) {
498
+ const handles = points.map((point) => imageData.worldToIndex(point));
499
+ const calibrate = getCalibratedLengthUnitsAndScale(image, handles);
500
+ const radius = CircleROITool.calculateLengthInIndex(calibrate, handles);
501
+ const area = Math.PI * radius * radius;
502
+ const perimeter = 2 * Math.PI * radius;
503
+ const isEmptyArea = radius === 0;
504
+ const { unit, areaUnit } = calibrate;
505
+ const namedArea = {
506
+ name: 'area',
507
+ value: area,
508
+ unit: areaUnit,
509
+ type: MeasurementType.Area,
510
+ };
511
+ const namedCircumference = {
512
+ name: 'circumference',
513
+ value: perimeter,
514
+ unit,
515
+ type: MeasurementType.Linear,
516
+ };
517
+ const namedRadius = {
518
+ name: 'radius',
519
+ value: radius,
520
+ unit,
521
+ type: MeasurementType.Linear,
522
+ };
523
+ const statsArray = [namedArea, namedRadius, namedCircumference];
524
+ cachedStats[targetId] = {
525
+ Modality: metadata.Modality,
526
+ area,
527
+ isEmptyArea,
528
+ areaUnit,
529
+ radius,
530
+ radiusUnit: unit,
531
+ perimeter,
532
+ statsArray,
533
+ };
534
+ const pos1Index = transformWorldToIndex(imageData, topLeftWorld);
535
+ const pos2Index = transformWorldToIndex(imageData, bottomRightWorld);
536
+ this.isHandleOutsideImage = !BaseTool.isInsideVolume(dimensions, [
537
+ pos1Index,
538
+ pos2Index,
539
+ ]);
540
+ if (!this.isHandleOutsideImage) {
511
541
  const iMin = Math.min(pos1Index[0], pos2Index[0]);
512
542
  const iMax = Math.max(pos1Index[0], pos2Index[0]);
513
543
  const jMin = Math.min(pos1Index[1], pos2Index[1]);
@@ -529,14 +559,6 @@ class CircleROITool extends AnnotationTool {
529
559
  yRadius: yRadius < EPSILON / 2 ? 0 : yRadius,
530
560
  zRadius: zRadius < EPSILON / 2 ? 0 : zRadius,
531
561
  };
532
- const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints(viewPlaneNormal, viewUp, worldPos1, worldPos2);
533
- const isEmptyArea = worldWidth === 0 && worldHeight === 0;
534
- const handles = [pos1Index, pos2Index];
535
- const { scale, unit, areaUnit } = getCalibratedLengthUnitsAndScale(image, handles);
536
- const aspect = getCalibratedAspect(image);
537
- const area = Math.abs(Math.PI *
538
- (worldWidth / scale / 2) *
539
- (worldHeight / aspect / scale / 2));
540
562
  const pixelUnitsOptions = {
541
563
  isPreScaled: isViewportPreScaled(viewport, targetId),
542
564
  isSuvScaled: this.isSuvScaled(viewport, targetId, annotation.metadata.referencedImageId),
@@ -553,26 +575,15 @@ class CircleROITool extends AnnotationTool {
553
575
  }
554
576
  const stats = this.configuration.statsCalculator.getStatistics();
555
577
  cachedStats[targetId] = {
578
+ ...cachedStats[targetId],
556
579
  Modality: metadata.Modality,
557
- area,
558
580
  mean: stats.mean?.value,
559
581
  max: stats.max?.value,
560
582
  min: stats.min?.value,
561
583
  pointsInShape,
562
584
  stdDev: stats.stdDev?.value,
563
- statsArray: stats.array,
564
- isEmptyArea,
565
- areaUnit,
566
- radius: worldWidth / 2 / scale,
567
- radiusUnit: unit,
568
- perimeter: (2 * Math.PI * (worldWidth / 2)) / scale,
569
585
  modalityUnit,
570
- };
571
- }
572
- else {
573
- this.isHandleOutsideImage = true;
574
- cachedStats[targetId] = {
575
- Modality: metadata.Modality,
586
+ statsArray: [...statsArray, ...stats.array],
576
587
  };
577
588
  }
578
589
  }
@@ -582,10 +593,6 @@ class CircleROITool extends AnnotationTool {
582
593
  }
583
594
  return cachedStats;
584
595
  };
585
- this._isInsideVolume = (index1, index2, dimensions) => {
586
- return (csUtils.indexWithinDimensions(index1, dimensions) &&
587
- csUtils.indexWithinDimensions(index2, dimensions));
588
- };
589
596
  this._throttledCalculateCachedStats = throttle(this._calculateCachedStats, 100, { trailing: true });
590
597
  }
591
598
  static { this.hydrate = (viewportId, points, options) => {