@cornerstonejs/tools 1.83.4 → 1.84.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/tools",
3
- "version": "1.83.4",
3
+ "version": "1.84.1",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "main": "src/index.ts",
6
6
  "types": "dist/types/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js"
30
30
  },
31
31
  "dependencies": {
32
- "@cornerstonejs/core": "^1.83.4",
32
+ "@cornerstonejs/core": "^1.84.1",
33
33
  "@icr/polyseg-wasm": "0.4.0",
34
34
  "@types/offscreencanvas": "2019.7.3",
35
35
  "comlink": "^4.4.1",
@@ -59,5 +59,5 @@
59
59
  "type": "individual",
60
60
  "url": "https://ohif.org/donate"
61
61
  },
62
- "gitHead": "864a3950e445c6c7631c8eee4342e86038b69267"
62
+ "gitHead": "e14af95b33e92828f3e0c8c21f0dc9319647abda"
63
63
  }
@@ -7,6 +7,7 @@ import filterToolsWithAnnotationsForElement from '../../store/filterToolsWithAnn
7
7
  import getToolsWithModesForMouseEvent from '../shared/getToolsWithModesForMouseEvent';
8
8
  import triggerAnnotationRender from '../../utilities/triggerAnnotationRender';
9
9
  import { MouseMoveEventType } from '../../types/EventTypes';
10
+ import { initElementCursor } from '../../cursors/elementCursor';
10
11
 
11
12
  const { Active, Passive } = ToolModes;
12
13
 
@@ -67,4 +68,8 @@ export default function mouseMove(evt: MouseMoveEventType) {
67
68
  if (annotationsNeedToBeRedrawn === true) {
68
69
  triggerAnnotationRender(element);
69
70
  }
71
+
72
+ if (!state.isInteractingWithTool) {
73
+ initElementCursor(element, null);
74
+ }
70
75
  }
@@ -3,11 +3,17 @@ import {
3
3
  getEnabledElement,
4
4
  utilities as csUtils,
5
5
  VolumeViewport,
6
+ utilities,
7
+ triggerEvent,
8
+ eventTarget,
6
9
  } from '@cornerstonejs/core';
7
10
  import type { Types } from '@cornerstonejs/core';
8
11
 
9
12
  import { removeAnnotation } from '../../stateManagement/annotation/annotationState';
10
- import { drawHandles as drawHandlesSvg } from '../../drawingSvg';
13
+ import {
14
+ drawHandles as drawHandlesSvg,
15
+ drawLinkedTextBox as drawLinkedTextBoxSvg,
16
+ } from '../../drawingSvg';
11
17
  import { state } from '../../store';
12
18
  import { Events, KeyboardBindings, ChangeTypes } from '../../enums';
13
19
  import { resetElementCursor } from '../../cursors/elementCursor';
@@ -17,9 +23,16 @@ import type {
17
23
  PublicToolProps,
18
24
  ToolProps,
19
25
  SVGDrawingHelper,
26
+ TextBoxHandle,
20
27
  } from '../../types';
21
28
  import getMouseModifierKey from '../../eventDispatchers/shared/getMouseModifier';
22
- import { math, triggerAnnotationRenderForViewportIds } from '../../utilities';
29
+ import {
30
+ getCalibratedLengthUnitsAndScale,
31
+ math,
32
+ roundNumber,
33
+ throttle,
34
+ triggerAnnotationRenderForViewportIds,
35
+ } from '../../utilities';
23
36
  import findHandlePolylineIndex from '../../utilities/contours/findHandlePolylineIndex';
24
37
  import { LivewireContourAnnotation } from '../../types/ToolSpecificAnnotationTypes';
25
38
  import { ContourWindingDirection } from '../../types/ContourAnnotation';
@@ -32,6 +45,8 @@ import { LivewireScissors } from '../../utilities/livewire/LivewireScissors';
32
45
  import { LivewirePath } from '../../utilities/livewire/LiveWirePath';
33
46
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
34
47
  import ContourSegmentationBaseTool from '../base/ContourSegmentationBaseTool';
48
+ import { AnnotationModifiedEventDetail } from '../../types/EventTypes';
49
+ import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
35
50
 
36
51
  const CLICK_CLOSE_CURVE_SQR_DIST = 10 ** 2; // px
37
52
 
@@ -43,10 +58,12 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
43
58
 
44
59
  touchDragCallback: any;
45
60
  mouseDragCallback: any;
61
+ _throttledCalculateCachedStats: any;
46
62
  editData: {
47
63
  annotation: LivewireContourAnnotation;
48
64
  viewportIdsToRender: Array<string>;
49
65
  handleIndex?: number;
66
+ movingTextBox?: boolean;
50
67
  newAnnotation?: boolean;
51
68
  hasMoved?: boolean;
52
69
  lastCanvasPoint?: Types.Point2;
@@ -68,6 +85,8 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
68
85
  defaultToolProps: ToolProps = {
69
86
  supportedInteractionTypes: ['Mouse', 'Touch'],
70
87
  configuration: {
88
+ getTextLines: defaultGetTextLines,
89
+ calculateStats: true,
71
90
  preventHandleOutsideImage: false,
72
91
  /**
73
92
  * Specify which modifier key is used to add a hole to a contour. The
@@ -135,6 +154,11 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
135
154
  }
136
155
  ) {
137
156
  super(toolProps, defaultToolProps);
157
+ this._throttledCalculateCachedStats = throttle(
158
+ this._calculateCachedStats,
159
+ 100,
160
+ { trailing: true }
161
+ );
138
162
  }
139
163
 
140
164
  protected setupBaseEditData(
@@ -365,6 +389,7 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
365
389
  this.editData = {
366
390
  annotation,
367
391
  viewportIdsToRender,
392
+ movingTextBox: false,
368
393
  };
369
394
 
370
395
  const enabledElement = getEnabledElement(element);
@@ -386,8 +411,16 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
386
411
 
387
412
  annotation.highlighted = true;
388
413
 
389
- const { points } = data.handles;
390
- const handleIndex = points.findIndex((p) => p === handle);
414
+ let movingTextBox = false;
415
+ let handleIndex;
416
+
417
+ if ((handle as TextBoxHandle).worldPosition) {
418
+ movingTextBox = true;
419
+ } else {
420
+ const { points } = data.handles;
421
+
422
+ handleIndex = points.findIndex((p) => p === handle);
423
+ }
391
424
 
392
425
  // Find viewports to render on drag.
393
426
  const viewportIdsToRender = getViewportIdsWithToolToRender(
@@ -399,6 +432,7 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
399
432
  annotation,
400
433
  viewportIdsToRender,
401
434
  handleIndex,
435
+ movingTextBox,
402
436
  };
403
437
  this._activateModify(element);
404
438
 
@@ -719,6 +753,7 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
719
753
 
720
754
  annotation.invalidated = true;
721
755
  editData.hasMoved = true;
756
+ editData.closed = true;
722
757
  }
723
758
 
724
759
  private _dragCallback = (evt: EventTypes.InteractionEventType): void => {
@@ -726,10 +761,25 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
726
761
  const eventDetail = evt.detail;
727
762
  const { element } = eventDetail;
728
763
 
729
- const { annotation, viewportIdsToRender, handleIndex } = this.editData;
730
- if (handleIndex === undefined) {
731
- // Drag mode - moving object
732
- console.warn('No drag implemented for livewire');
764
+ const { annotation, viewportIdsToRender, handleIndex, movingTextBox } =
765
+ this.editData;
766
+ const { data } = annotation;
767
+
768
+ if (movingTextBox) {
769
+ // Drag mode - moving text box
770
+ const { deltaPoints } = eventDetail as EventTypes.MouseDragEventDetail;
771
+ const worldPosDelta = deltaPoints.world;
772
+
773
+ const { textBox } = data.handles;
774
+ const { worldPosition } = textBox;
775
+
776
+ worldPosition[0] += worldPosDelta[0];
777
+ worldPosition[1] += worldPosDelta[1];
778
+ worldPosition[2] += worldPosDelta[2];
779
+
780
+ textBox.hasMoved = true;
781
+ } else if (handleIndex === undefined) {
782
+ console.warn('Drag annotation not implemented');
733
783
  } else {
734
784
  // Move mode - after double click, and mouse move to draw
735
785
  const { currentPoints } = eventDetail;
@@ -737,6 +787,8 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
737
787
  this.editHandle(worldPos, element, annotation, handleIndex);
738
788
  }
739
789
 
790
+ this.editData.hasMoved = true;
791
+
740
792
  const enabledElement = getEnabledElement(element);
741
793
  const { renderingEngine } = enabledElement;
742
794
 
@@ -879,10 +931,16 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
879
931
  annotationStyle: Record<string, any>;
880
932
  svgDrawingHelper: SVGDrawingHelper;
881
933
  }): boolean {
882
- const { annotation, enabledElement, svgDrawingHelper, annotationStyle } =
883
- renderContext;
934
+ const {
935
+ annotation,
936
+ enabledElement,
937
+ svgDrawingHelper,
938
+ annotationStyle,
939
+ targetId,
940
+ } = renderContext;
884
941
 
885
942
  const { viewport } = enabledElement;
943
+ const { element } = viewport;
886
944
  const { worldToCanvas } = viewport;
887
945
  const { annotationUID, data, highlighted } = annotation;
888
946
  const { handles } = data;
@@ -916,9 +974,201 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
916
974
  // Let the base class render the contour
917
975
  super.renderAnnotationInstance(renderContext);
918
976
 
977
+ if (
978
+ !data.cachedStats[targetId] ||
979
+ data.cachedStats[targetId].areaUnit == null
980
+ ) {
981
+ data.cachedStats[targetId] = {
982
+ Modality: null,
983
+ area: null,
984
+ areaUnit: null,
985
+ };
986
+
987
+ this._calculateCachedStats(annotation, element);
988
+ } else if (annotation.invalidated) {
989
+ this._throttledCalculateCachedStats(annotation, element);
990
+ }
991
+
992
+ this._renderStats(
993
+ annotation,
994
+ viewport,
995
+ svgDrawingHelper,
996
+ annotationStyle.textbox
997
+ );
998
+
919
999
  return true;
920
1000
  }
921
1001
 
1002
+ private _calculateCachedStats = (
1003
+ annotation: LivewireContourAnnotation,
1004
+ element: HTMLDivElement
1005
+ ) => {
1006
+ if (!this.configuration.calculateStats) {
1007
+ return;
1008
+ }
1009
+ const data = annotation.data;
1010
+
1011
+ if (!data.contour.closed) {
1012
+ return;
1013
+ }
1014
+
1015
+ const enabledElement = getEnabledElement(element);
1016
+ const { viewport, renderingEngine } = enabledElement;
1017
+ const { cachedStats } = data;
1018
+ const { polyline: points } = data.contour;
1019
+ const targetIds = Object.keys(cachedStats);
1020
+
1021
+ for (let i = 0; i < targetIds.length; i++) {
1022
+ const targetId = targetIds[i];
1023
+ const image = this.getTargetIdImage(targetId, renderingEngine);
1024
+
1025
+ if (!image) {
1026
+ continue;
1027
+ }
1028
+
1029
+ const { metadata } = image;
1030
+ const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
1031
+
1032
+ const canvasPoint = canvasCoordinates[0];
1033
+ const originalWorldPoint = viewport.canvasToWorld(canvasPoint);
1034
+ const deltaXPoint = viewport.canvasToWorld([
1035
+ canvasPoint[0] + 1,
1036
+ canvasPoint[1],
1037
+ ]);
1038
+ const deltaYPoint = viewport.canvasToWorld([
1039
+ canvasPoint[0],
1040
+ canvasPoint[1] + 1,
1041
+ ]);
1042
+
1043
+ const deltaInX = vec3.distance(originalWorldPoint, deltaXPoint);
1044
+ const deltaInY = vec3.distance(originalWorldPoint, deltaYPoint);
1045
+
1046
+ const { imageData } = image;
1047
+ const { scale, areaUnits } = getCalibratedLengthUnitsAndScale(
1048
+ image,
1049
+ () => {
1050
+ const {
1051
+ maxX: canvasMaxX,
1052
+ maxY: canvasMaxY,
1053
+ minX: canvasMinX,
1054
+ minY: canvasMinY,
1055
+ } = math.polyline.getAABB(canvasCoordinates);
1056
+
1057
+ const topLeftBBWorld = viewport.canvasToWorld([
1058
+ canvasMinX,
1059
+ canvasMinY,
1060
+ ]);
1061
+
1062
+ const topLeftBBIndex = utilities.transformWorldToIndex(
1063
+ imageData,
1064
+ topLeftBBWorld
1065
+ );
1066
+
1067
+ const bottomRightBBWorld = viewport.canvasToWorld([
1068
+ canvasMaxX,
1069
+ canvasMaxY,
1070
+ ]);
1071
+
1072
+ const bottomRightBBIndex = utilities.transformWorldToIndex(
1073
+ imageData,
1074
+ bottomRightBBWorld
1075
+ );
1076
+
1077
+ return [topLeftBBIndex, bottomRightBBIndex];
1078
+ }
1079
+ );
1080
+ let area = math.polyline.getArea(canvasCoordinates) / scale / scale;
1081
+
1082
+ // Convert from canvas_pixels ^2 to mm^2
1083
+ area *= deltaInX * deltaInY;
1084
+
1085
+ cachedStats[targetId] = {
1086
+ Modality: metadata.Modality,
1087
+ area,
1088
+ areaUnit: areaUnits,
1089
+ };
1090
+ }
1091
+
1092
+ this.triggerAnnotationModified(
1093
+ annotation,
1094
+ enabledElement,
1095
+ ChangeTypes.StatsUpdated
1096
+ );
1097
+
1098
+ return cachedStats;
1099
+ };
1100
+
1101
+ private _renderStats = (
1102
+ annotation,
1103
+ viewport,
1104
+ svgDrawingHelper,
1105
+ textboxStyle
1106
+ ) => {
1107
+ const data = annotation.data;
1108
+ const targetId = this.getTargetId(viewport);
1109
+
1110
+ if (!data.contour.closed || !textboxStyle.visibility) {
1111
+ return;
1112
+ }
1113
+
1114
+ const textLines = this.configuration.getTextLines(data, targetId);
1115
+ if (!textLines || textLines.length === 0) {
1116
+ return;
1117
+ }
1118
+
1119
+ const canvasCoordinates = data.handles.points.map((p) =>
1120
+ viewport.worldToCanvas(p)
1121
+ );
1122
+ if (!data.handles.textBox.hasMoved) {
1123
+ const canvasTextBoxCoords = getTextBoxCoordsCanvas(canvasCoordinates);
1124
+
1125
+ data.handles.textBox.worldPosition =
1126
+ viewport.canvasToWorld(canvasTextBoxCoords);
1127
+ }
1128
+
1129
+ const textBoxPosition = viewport.worldToCanvas(
1130
+ data.handles.textBox.worldPosition
1131
+ );
1132
+
1133
+ const textBoxUID = 'textBox';
1134
+ const boundingBox = drawLinkedTextBoxSvg(
1135
+ svgDrawingHelper,
1136
+ annotation.annotationUID ?? '',
1137
+ textBoxUID,
1138
+ textLines,
1139
+ textBoxPosition,
1140
+ canvasCoordinates,
1141
+ {},
1142
+ textboxStyle
1143
+ );
1144
+
1145
+ const { x: left, y: top, width, height } = boundingBox;
1146
+
1147
+ data.handles.textBox.worldBoundingBox = {
1148
+ topLeft: viewport.canvasToWorld([left, top]),
1149
+ topRight: viewport.canvasToWorld([left + width, top]),
1150
+ bottomLeft: viewport.canvasToWorld([left, top + height]),
1151
+ bottomRight: viewport.canvasToWorld([left + width, top + height]),
1152
+ };
1153
+ };
1154
+
1155
+ triggerAnnotationModified = (
1156
+ annotation: LivewireContourAnnotation,
1157
+ enabledElement: Types.IEnabledElement,
1158
+ changeType = ChangeTypes.StatsUpdated
1159
+ ): void => {
1160
+ const { viewportId, renderingEngineId } = enabledElement;
1161
+ const eventType = Events.ANNOTATION_MODIFIED;
1162
+ const eventDetail: AnnotationModifiedEventDetail = {
1163
+ annotation,
1164
+ viewportId,
1165
+ renderingEngineId,
1166
+ changeType,
1167
+ };
1168
+
1169
+ triggerEvent(eventTarget, eventType, eventDetail);
1170
+ };
1171
+
922
1172
  protected updateAnnotation(livewirePath: LivewirePath) {
923
1173
  if (!this.editData || !livewirePath) {
924
1174
  return;
@@ -954,3 +1204,17 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
954
1204
 
955
1205
  LivewireContourTool.toolName = 'LivewireContour';
956
1206
  export default LivewireContourTool;
1207
+
1208
+ function defaultGetTextLines(data, targetId): string[] {
1209
+ const cachedVolumeStats = data.cachedStats[targetId];
1210
+ const { area, areaUnit } = cachedVolumeStats;
1211
+ const textLines: string[] = [];
1212
+
1213
+ if (area) {
1214
+ const areaLine = `Area: ${roundNumber(area)} ${areaUnit}`;
1215
+
1216
+ textLines.push(areaLine);
1217
+ }
1218
+
1219
+ return textLines;
1220
+ }