@cornerstonejs/tools 1.38.1 → 1.39.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/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/LivewireContourTool.d.ts +44 -0
- package/dist/cjs/tools/annotation/LivewireContourTool.js +442 -0
- package/dist/cjs/tools/annotation/LivewireContourTool.js.map +1 -0
- package/dist/cjs/tools/index.d.ts +2 -1
- package/dist/cjs/tools/index.js +3 -1
- package/dist/cjs/tools/index.js.map +1 -1
- package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +10 -0
- package/dist/cjs/utilities/BucketQueue.d.ts +20 -0
- package/dist/cjs/utilities/BucketQueue.js +83 -0
- package/dist/cjs/utilities/BucketQueue.js.map +1 -0
- package/dist/cjs/utilities/livewire/LiveWirePath.d.ts +16 -0
- package/dist/cjs/utilities/livewire/LiveWirePath.js +64 -0
- package/dist/cjs/utilities/livewire/LiveWirePath.js.map +1 -0
- package/dist/cjs/utilities/livewire/LivewireScissors.d.ts +37 -0
- package/dist/cjs/utilities/livewire/LivewireScissors.js +281 -0
- package/dist/cjs/utilities/livewire/LivewireScissors.js.map +1 -0
- package/dist/cjs/utilities/math/vec2/liangBarksyClip.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tools/annotation/LivewireContourTool.js +439 -0
- package/dist/esm/tools/annotation/LivewireContourTool.js.map +1 -0
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/esm/utilities/BucketQueue.js +79 -0
- package/dist/esm/utilities/BucketQueue.js.map +1 -0
- package/dist/esm/utilities/livewire/LiveWirePath.js +60 -0
- package/dist/esm/utilities/livewire/LiveWirePath.js.map +1 -0
- package/dist/esm/utilities/livewire/LivewireScissors.js +277 -0
- package/dist/esm/utilities/livewire/LivewireScissors.js.map +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/tools/annotation/LivewireContourTool.d.ts +45 -0
- package/dist/types/tools/annotation/LivewireContourTool.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/types/ToolSpecificAnnotationTypes.d.ts +10 -0
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
- package/dist/types/utilities/BucketQueue.d.ts +21 -0
- package/dist/types/utilities/BucketQueue.d.ts.map +1 -0
- package/dist/types/utilities/livewire/LiveWirePath.d.ts +17 -0
- package/dist/types/utilities/livewire/LiveWirePath.d.ts.map +1 -0
- package/dist/types/utilities/livewire/LivewireScissors.d.ts +38 -0
- package/dist/types/utilities/livewire/LivewireScissors.d.ts.map +1 -0
- package/dist/types/utilities/math/vec2/liangBarksyClip.d.ts +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/tools/annotation/LivewireContourTool.ts +799 -0
- package/src/tools/index.ts +2 -0
- package/src/types/ToolSpecificAnnotationTypes.ts +12 -0
- package/src/utilities/BucketQueue.ts +154 -0
- package/src/utilities/livewire/LiveWirePath.ts +131 -0
- package/src/utilities/livewire/LivewireScissors.ts +582 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { glMatrix, mat4, vec3 } from 'gl-matrix';
|
|
2
|
+
import { AnnotationTool } from '../base';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getEnabledElement,
|
|
6
|
+
eventTarget,
|
|
7
|
+
triggerEvent,
|
|
8
|
+
utilities as csUtils,
|
|
9
|
+
StackViewport,
|
|
10
|
+
VolumeViewport,
|
|
11
|
+
} from '@cornerstonejs/core';
|
|
12
|
+
import type { Types } from '@cornerstonejs/core';
|
|
13
|
+
import {
|
|
14
|
+
addAnnotation,
|
|
15
|
+
getAnnotations,
|
|
16
|
+
removeAnnotation,
|
|
17
|
+
} from '../../stateManagement/annotation/annotationState';
|
|
18
|
+
import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
|
|
19
|
+
import {
|
|
20
|
+
drawHandles as drawHandlesSvg,
|
|
21
|
+
drawPolyline as drawPolylineSvg,
|
|
22
|
+
} from '../../drawingSvg';
|
|
23
|
+
import { state } from '../../store';
|
|
24
|
+
import { Events } from '../../enums';
|
|
25
|
+
import { resetElementCursor } from '../../cursors/elementCursor';
|
|
26
|
+
import {
|
|
27
|
+
EventTypes,
|
|
28
|
+
ToolHandle,
|
|
29
|
+
PublicToolProps,
|
|
30
|
+
ToolProps,
|
|
31
|
+
SVGDrawingHelper,
|
|
32
|
+
} from '../../types';
|
|
33
|
+
import {
|
|
34
|
+
math,
|
|
35
|
+
viewportFilters,
|
|
36
|
+
triggerAnnotationRenderForViewportIds,
|
|
37
|
+
} from '../../utilities';
|
|
38
|
+
import { LivewireContourAnnotation } from '../../types/ToolSpecificAnnotationTypes';
|
|
39
|
+
import {
|
|
40
|
+
AnnotationCompletedEventDetail,
|
|
41
|
+
AnnotationModifiedEventDetail,
|
|
42
|
+
} from '../../types/EventTypes';
|
|
43
|
+
import { StyleSpecifier } from '../../types/AnnotationStyle';
|
|
44
|
+
|
|
45
|
+
import { LivewireScissors } from '../../utilities/livewire/LivewireScissors';
|
|
46
|
+
import { LivewirePath } from '../../utilities/livewire/LiveWirePath';
|
|
47
|
+
|
|
48
|
+
const { getViewportIdsWithToolToRender } = viewportFilters;
|
|
49
|
+
const CLICK_CLOSE_CURVE_SQR_DIST = 10 ** 2; // px
|
|
50
|
+
|
|
51
|
+
class LivewireContourTool extends AnnotationTool {
|
|
52
|
+
public static toolName: string;
|
|
53
|
+
private scissors: LivewireScissors;
|
|
54
|
+
|
|
55
|
+
touchDragCallback: any;
|
|
56
|
+
mouseDragCallback: any;
|
|
57
|
+
editData: {
|
|
58
|
+
annotation: LivewireContourAnnotation;
|
|
59
|
+
viewportIdsToRender: Array<string>;
|
|
60
|
+
handleIndex?: number;
|
|
61
|
+
newAnnotation?: boolean;
|
|
62
|
+
hasMoved?: boolean;
|
|
63
|
+
lastCanvasPoint?: Types.Point2;
|
|
64
|
+
confirmedPath?: LivewirePath;
|
|
65
|
+
currentPath?: LivewirePath;
|
|
66
|
+
closed?: boolean;
|
|
67
|
+
worldToSlice?: (point: Types.Point3) => Types.Point2;
|
|
68
|
+
sliceToWorld?: (point: Types.Point2) => Types.Point3;
|
|
69
|
+
} | null;
|
|
70
|
+
isDrawing: boolean;
|
|
71
|
+
isHandleOutsideImage = false;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
toolProps: PublicToolProps = {},
|
|
75
|
+
defaultToolProps: ToolProps = {
|
|
76
|
+
supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
77
|
+
configuration: {
|
|
78
|
+
preventHandleOutsideImage: false,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
) {
|
|
82
|
+
super(toolProps, defaultToolProps);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Based on the current position of the mouse and the current imageId to create
|
|
87
|
+
* a CircleROI Annotation and stores it in the annotationManager
|
|
88
|
+
*
|
|
89
|
+
* @param evt - EventTypes.NormalizedMouseEventType
|
|
90
|
+
* @returns The annotation object.
|
|
91
|
+
*
|
|
92
|
+
*/
|
|
93
|
+
addNewAnnotation = (
|
|
94
|
+
evt: EventTypes.InteractionEventType
|
|
95
|
+
): LivewireContourAnnotation => {
|
|
96
|
+
const eventDetail = evt.detail;
|
|
97
|
+
const { currentPoints, element } = eventDetail;
|
|
98
|
+
const { world: worldPos, canvas: canvasPos } = currentPoints;
|
|
99
|
+
|
|
100
|
+
const enabledElement = getEnabledElement(element);
|
|
101
|
+
const { viewport, renderingEngine } = enabledElement;
|
|
102
|
+
|
|
103
|
+
this.isDrawing = true;
|
|
104
|
+
|
|
105
|
+
const camera = viewport.getCamera();
|
|
106
|
+
const { viewPlaneNormal, viewUp } = camera;
|
|
107
|
+
|
|
108
|
+
const referencedImageId = this.getReferencedImageId(
|
|
109
|
+
viewport,
|
|
110
|
+
worldPos,
|
|
111
|
+
viewPlaneNormal,
|
|
112
|
+
viewUp
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
|
|
116
|
+
const defaultActor = viewport.getDefaultActor();
|
|
117
|
+
|
|
118
|
+
if (!defaultActor || !csUtils.isImageActor(defaultActor)) {
|
|
119
|
+
throw new Error('Default actor must be an image actor');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const viewportImageData = viewport.getImageData();
|
|
123
|
+
const { imageData: vtkImageData } = viewportImageData;
|
|
124
|
+
let worldToSlice: (point: Types.Point3) => Types.Point2;
|
|
125
|
+
let sliceToWorld: (point: Types.Point2) => Types.Point3;
|
|
126
|
+
let scalarData;
|
|
127
|
+
let width;
|
|
128
|
+
let height;
|
|
129
|
+
|
|
130
|
+
if (viewport instanceof StackViewport) {
|
|
131
|
+
scalarData = viewportImageData.scalarData;
|
|
132
|
+
width = viewportImageData.dimensions[0];
|
|
133
|
+
height = viewportImageData.dimensions[1];
|
|
134
|
+
|
|
135
|
+
// Method only to simplify the code making stack and volume viewports code
|
|
136
|
+
// similar and avoiding `if(stack)/else` whenever a coordinate needs to be
|
|
137
|
+
// transformed because `worldToSlice` in this case returns the same IJK
|
|
138
|
+
// coordinate from index space.
|
|
139
|
+
worldToSlice = (point: Types.Point3) => {
|
|
140
|
+
const ijkPoint = csUtils.transformWorldToIndex(vtkImageData, point);
|
|
141
|
+
return [ijkPoint[0], ijkPoint[1]];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Method only to simplify the code making stack and volume viewports code
|
|
145
|
+
// similar and avoiding `if(stack)/else` whenever a coordinate needs to be
|
|
146
|
+
// transformed because `sliceToWorld` in this case receives the same IJK
|
|
147
|
+
// coordinate from index space.
|
|
148
|
+
sliceToWorld = (point: Types.Point2) =>
|
|
149
|
+
csUtils.transformIndexToWorld(vtkImageData, [point[0], point[1], 0]);
|
|
150
|
+
} else if (viewport instanceof VolumeViewport) {
|
|
151
|
+
const sliceImageData = csUtils.getCurrentVolumeViewportSlice(viewport);
|
|
152
|
+
const { sliceToIndexMatrix, indexToSliceMatrix } = sliceImageData;
|
|
153
|
+
|
|
154
|
+
worldToSlice = (point: Types.Point3) => {
|
|
155
|
+
const ijkPoint = csUtils.transformWorldToIndex(vtkImageData, point);
|
|
156
|
+
const slicePoint = vec3.transformMat4(
|
|
157
|
+
[0, 0, 0],
|
|
158
|
+
ijkPoint,
|
|
159
|
+
indexToSliceMatrix
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return [slicePoint[0], slicePoint[1]];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
sliceToWorld = (point: Types.Point2) => {
|
|
166
|
+
const ijkPoint = vec3.transformMat4(
|
|
167
|
+
[0, 0, 0],
|
|
168
|
+
[point[0], point[1], 0],
|
|
169
|
+
sliceToIndexMatrix
|
|
170
|
+
) as Types.Point3;
|
|
171
|
+
|
|
172
|
+
return csUtils.transformIndexToWorld(vtkImageData, ijkPoint);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
scalarData = sliceImageData.scalarData;
|
|
176
|
+
width = sliceImageData.width;
|
|
177
|
+
height = sliceImageData.height;
|
|
178
|
+
} else {
|
|
179
|
+
throw new Error('Viewport not supported');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { voiRange } = viewport.getProperties();
|
|
183
|
+
const startPos = worldToSlice(worldPos);
|
|
184
|
+
|
|
185
|
+
this.scissors = LivewireScissors.createInstanceFromRawPixelData(
|
|
186
|
+
scalarData,
|
|
187
|
+
width,
|
|
188
|
+
height,
|
|
189
|
+
voiRange
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
this.scissors.startSearch(startPos);
|
|
193
|
+
|
|
194
|
+
const confirmedPath = new LivewirePath();
|
|
195
|
+
const currentPath = new LivewirePath();
|
|
196
|
+
|
|
197
|
+
confirmedPath.addPoint(startPos);
|
|
198
|
+
confirmedPath.addControlPoint(startPos);
|
|
199
|
+
|
|
200
|
+
const annotation: LivewireContourAnnotation = {
|
|
201
|
+
highlighted: true,
|
|
202
|
+
invalidated: true,
|
|
203
|
+
metadata: {
|
|
204
|
+
toolName: this.getToolName(),
|
|
205
|
+
viewPlaneNormal: <Types.Point3>[...viewPlaneNormal],
|
|
206
|
+
viewUp: <Types.Point3>[...viewUp],
|
|
207
|
+
FrameOfReferenceUID,
|
|
208
|
+
referencedImageId,
|
|
209
|
+
},
|
|
210
|
+
data: {
|
|
211
|
+
polyline: [],
|
|
212
|
+
handles: {
|
|
213
|
+
points: [[...worldPos]],
|
|
214
|
+
activeHandleIndex: null,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
addAnnotation(annotation, element);
|
|
220
|
+
|
|
221
|
+
const viewportIdsToRender = getViewportIdsWithToolToRender(
|
|
222
|
+
element,
|
|
223
|
+
this.getToolName()
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
this.editData = {
|
|
227
|
+
annotation,
|
|
228
|
+
viewportIdsToRender,
|
|
229
|
+
newAnnotation: true,
|
|
230
|
+
hasMoved: false,
|
|
231
|
+
lastCanvasPoint: canvasPos,
|
|
232
|
+
confirmedPath: confirmedPath,
|
|
233
|
+
currentPath: currentPath,
|
|
234
|
+
closed: false,
|
|
235
|
+
worldToSlice,
|
|
236
|
+
sliceToWorld,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this._activateDraw(element);
|
|
240
|
+
evt.preventDefault();
|
|
241
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
242
|
+
|
|
243
|
+
return annotation;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* It returns if the canvas point is near the provided annotation in the provided
|
|
248
|
+
* element or not. A proximity is passed to the function to determine the
|
|
249
|
+
* proximity of the point to the annotation in number of pixels.
|
|
250
|
+
*
|
|
251
|
+
* @param element - HTML Element
|
|
252
|
+
* @param annotation - Annotation
|
|
253
|
+
* @param canvasCoords - Canvas coordinates
|
|
254
|
+
* @param proximity - Proximity to tool to consider
|
|
255
|
+
* @returns Boolean, whether the canvas point is near tool
|
|
256
|
+
*/
|
|
257
|
+
isPointNearTool = (
|
|
258
|
+
element: HTMLDivElement,
|
|
259
|
+
annotation: LivewireContourAnnotation,
|
|
260
|
+
canvasCoords: Types.Point2,
|
|
261
|
+
proximity: number
|
|
262
|
+
): boolean => {
|
|
263
|
+
const enabledElement = getEnabledElement(element);
|
|
264
|
+
const { viewport } = enabledElement;
|
|
265
|
+
const proximitySquared = proximity * proximity;
|
|
266
|
+
const canvasPoints = annotation.data.polyline.map((p) =>
|
|
267
|
+
viewport.worldToCanvas(p)
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
let startPoint = canvasPoints[canvasPoints.length - 1];
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < canvasPoints.length; i++) {
|
|
273
|
+
const endPoint = canvasPoints[i];
|
|
274
|
+
const distanceToPointSquared = math.lineSegment.distanceToPointSquared(
|
|
275
|
+
startPoint,
|
|
276
|
+
endPoint,
|
|
277
|
+
canvasCoords
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (distanceToPointSquared <= proximitySquared) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
startPoint = endPoint;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return false;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
toolSelectedCallback = (
|
|
291
|
+
evt: EventTypes.InteractionEventType,
|
|
292
|
+
annotation: LivewireContourAnnotation
|
|
293
|
+
): void => {
|
|
294
|
+
const eventDetail = evt.detail;
|
|
295
|
+
const { element } = eventDetail;
|
|
296
|
+
|
|
297
|
+
annotation.highlighted = true;
|
|
298
|
+
|
|
299
|
+
const viewportIdsToRender = getViewportIdsWithToolToRender(
|
|
300
|
+
element,
|
|
301
|
+
this.getToolName()
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
this.editData = {
|
|
305
|
+
annotation,
|
|
306
|
+
viewportIdsToRender,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const enabledElement = getEnabledElement(element);
|
|
310
|
+
const { renderingEngine } = enabledElement;
|
|
311
|
+
|
|
312
|
+
this._activateModify(element);
|
|
313
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
314
|
+
evt.preventDefault();
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
handleSelectedCallback = (
|
|
318
|
+
evt: EventTypes.InteractionEventType,
|
|
319
|
+
annotation: LivewireContourAnnotation,
|
|
320
|
+
handle: ToolHandle
|
|
321
|
+
): void => {
|
|
322
|
+
const eventDetail = evt.detail;
|
|
323
|
+
const { element } = eventDetail;
|
|
324
|
+
const { data } = annotation;
|
|
325
|
+
|
|
326
|
+
annotation.highlighted = true;
|
|
327
|
+
|
|
328
|
+
const { points } = data.handles;
|
|
329
|
+
const handleIndex = points.findIndex((p) => p === handle);
|
|
330
|
+
|
|
331
|
+
// Find viewports to render on drag.
|
|
332
|
+
const viewportIdsToRender = getViewportIdsWithToolToRender(
|
|
333
|
+
element,
|
|
334
|
+
this.getToolName()
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
this.editData = {
|
|
338
|
+
annotation,
|
|
339
|
+
viewportIdsToRender,
|
|
340
|
+
handleIndex,
|
|
341
|
+
};
|
|
342
|
+
this._activateModify(element);
|
|
343
|
+
|
|
344
|
+
const enabledElement = getEnabledElement(element);
|
|
345
|
+
const { renderingEngine } = enabledElement;
|
|
346
|
+
|
|
347
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
348
|
+
|
|
349
|
+
evt.preventDefault();
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
_endCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
353
|
+
const eventDetail = evt.detail;
|
|
354
|
+
const { element } = eventDetail;
|
|
355
|
+
|
|
356
|
+
const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
|
|
357
|
+
const { data } = annotation;
|
|
358
|
+
|
|
359
|
+
data.handles.activeHandleIndex = null;
|
|
360
|
+
|
|
361
|
+
this._deactivateModify(element);
|
|
362
|
+
this._deactivateDraw(element);
|
|
363
|
+
|
|
364
|
+
resetElementCursor(element);
|
|
365
|
+
|
|
366
|
+
const enabledElement = getEnabledElement(element);
|
|
367
|
+
const { renderingEngine } = enabledElement;
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
this.isHandleOutsideImage &&
|
|
371
|
+
this.configuration.preventHandleOutsideImage
|
|
372
|
+
) {
|
|
373
|
+
removeAnnotation(annotation.annotationUID);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
377
|
+
|
|
378
|
+
if (newAnnotation) {
|
|
379
|
+
const eventType = Events.ANNOTATION_COMPLETED;
|
|
380
|
+
const eventDetail: AnnotationCompletedEventDetail = {
|
|
381
|
+
annotation,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
triggerEvent(eventTarget, eventType, eventDetail);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this.editData = null;
|
|
388
|
+
this.scissors = null;
|
|
389
|
+
this.isDrawing = false;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
private _mouseDownCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
393
|
+
const doubleClick = evt.type === Events.MOUSE_DOUBLE_CLICK;
|
|
394
|
+
const { annotation, viewportIdsToRender, worldToSlice, sliceToWorld } =
|
|
395
|
+
this.editData;
|
|
396
|
+
|
|
397
|
+
if (this.editData.closed) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const eventDetail = evt.detail;
|
|
402
|
+
const { element } = eventDetail;
|
|
403
|
+
const { currentPoints } = eventDetail;
|
|
404
|
+
const { canvas: canvasPos, world: worldPos } = currentPoints;
|
|
405
|
+
const enabledElement = getEnabledElement(element);
|
|
406
|
+
const { viewport, renderingEngine } = enabledElement;
|
|
407
|
+
const controlPoints = this.editData.currentPath.getControlPoints();
|
|
408
|
+
let closePath = controlPoints.length >= 2 && doubleClick;
|
|
409
|
+
let addNewPoint = true;
|
|
410
|
+
|
|
411
|
+
// Check if user clicked on the first point to close the curve
|
|
412
|
+
if (controlPoints.length >= 2) {
|
|
413
|
+
const closestHandlePoint = {
|
|
414
|
+
index: -1,
|
|
415
|
+
distSquared: Infinity,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Check if there is a control point close to the cursor
|
|
419
|
+
for (let i = 0, len = controlPoints.length; i < len; i++) {
|
|
420
|
+
const controlPoint = controlPoints[i];
|
|
421
|
+
const worldControlPoint = sliceToWorld(controlPoint);
|
|
422
|
+
const canvasControlPoint = viewport.worldToCanvas(worldControlPoint);
|
|
423
|
+
|
|
424
|
+
const distSquared = math.point.distanceToPointSquared(
|
|
425
|
+
canvasPos,
|
|
426
|
+
canvasControlPoint
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (
|
|
430
|
+
distSquared <= CLICK_CLOSE_CURVE_SQR_DIST &&
|
|
431
|
+
distSquared < closestHandlePoint.distSquared
|
|
432
|
+
) {
|
|
433
|
+
closestHandlePoint.distSquared = distSquared;
|
|
434
|
+
closestHandlePoint.index = i;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (closestHandlePoint.index === 0) {
|
|
439
|
+
addNewPoint = false;
|
|
440
|
+
closePath = true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.editData.closed = this.editData.closed || closePath;
|
|
445
|
+
|
|
446
|
+
if (addNewPoint) {
|
|
447
|
+
this.editData.confirmedPath = this.editData.currentPath;
|
|
448
|
+
|
|
449
|
+
// Add the current cursor position as a new control point after clicking
|
|
450
|
+
this.editData.confirmedPath.addControlPoint(
|
|
451
|
+
this.editData.currentPath.getLastPoint()
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Start a new search starting at the last control point
|
|
455
|
+
this.scissors.startSearch(worldToSlice(worldPos));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
annotation.invalidated = true;
|
|
459
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
460
|
+
|
|
461
|
+
if (this.editData.closed) {
|
|
462
|
+
// Update the annotation because `editData` will be set to null
|
|
463
|
+
this._updateAnnotation(element, this.editData.confirmedPath);
|
|
464
|
+
this._endCallback(evt);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
evt.preventDefault();
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
private _mouseMoveCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
471
|
+
const { element, currentPoints } = evt.detail;
|
|
472
|
+
const { world: worldPos, canvas: canvasPos } = currentPoints;
|
|
473
|
+
const { renderingEngine } = getEnabledElement(element);
|
|
474
|
+
const viewportIdsToRender = getViewportIdsWithToolToRender(
|
|
475
|
+
element,
|
|
476
|
+
this.getToolName()
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
this.editData.lastCanvasPoint = canvasPos;
|
|
480
|
+
|
|
481
|
+
const { width: imgWidth, height: imgHeight } = this.scissors;
|
|
482
|
+
const { worldToSlice } = this.editData;
|
|
483
|
+
const slicePoint: Types.Point2 = worldToSlice(worldPos);
|
|
484
|
+
|
|
485
|
+
// Check if the point is inside the bounding box
|
|
486
|
+
if (
|
|
487
|
+
slicePoint[0] < 0 ||
|
|
488
|
+
slicePoint[1] < 0 ||
|
|
489
|
+
slicePoint[0] >= imgWidth ||
|
|
490
|
+
slicePoint[1] >= imgHeight
|
|
491
|
+
) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const pathPoints = this.scissors.findPathToPoint(slicePoint);
|
|
496
|
+
const currentPath = new LivewirePath();
|
|
497
|
+
|
|
498
|
+
for (let i = 0, len = pathPoints.length; i < len; i++) {
|
|
499
|
+
currentPath.addPoint(pathPoints[i]);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Merge the "confirmed" path that goes from the first control point to the
|
|
503
|
+
// last one with the current path that goes from the last control point to
|
|
504
|
+
// the cursor point
|
|
505
|
+
currentPath.prependPath(this.editData.confirmedPath);
|
|
506
|
+
|
|
507
|
+
// Store the new path
|
|
508
|
+
this.editData.currentPath = currentPath;
|
|
509
|
+
|
|
510
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
511
|
+
evt.preventDefault();
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
private _dragCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
515
|
+
this.isDrawing = true;
|
|
516
|
+
const eventDetail = evt.detail;
|
|
517
|
+
const { element } = eventDetail;
|
|
518
|
+
|
|
519
|
+
const { annotation, viewportIdsToRender, handleIndex } = this.editData;
|
|
520
|
+
const { data } = annotation;
|
|
521
|
+
|
|
522
|
+
if (handleIndex === undefined) {
|
|
523
|
+
// Drag mode - moving handle
|
|
524
|
+
const { deltaPoints } = eventDetail as EventTypes.MouseDragEventDetail;
|
|
525
|
+
const worldPosDelta = deltaPoints.world;
|
|
526
|
+
|
|
527
|
+
const points = data.polyline;
|
|
528
|
+
|
|
529
|
+
points.forEach((point) => {
|
|
530
|
+
point[0] += worldPosDelta[0];
|
|
531
|
+
point[1] += worldPosDelta[1];
|
|
532
|
+
point[2] += worldPosDelta[2];
|
|
533
|
+
});
|
|
534
|
+
annotation.invalidated = true;
|
|
535
|
+
} else {
|
|
536
|
+
// Move mode - after double click, and mouse move to draw
|
|
537
|
+
const { currentPoints } = eventDetail;
|
|
538
|
+
const worldPos = currentPoints.world;
|
|
539
|
+
|
|
540
|
+
data.handles.points[handleIndex] = [...worldPos];
|
|
541
|
+
annotation.invalidated = true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
this.editData.hasMoved = true;
|
|
545
|
+
|
|
546
|
+
const enabledElement = getEnabledElement(element);
|
|
547
|
+
const { renderingEngine } = enabledElement;
|
|
548
|
+
|
|
549
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
cancel = (element: HTMLDivElement) => {
|
|
553
|
+
// If it is not in mid-draw or mid-modify
|
|
554
|
+
if (!this.isDrawing) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.isDrawing = false;
|
|
559
|
+
this._deactivateDraw(element);
|
|
560
|
+
this._deactivateModify(element);
|
|
561
|
+
resetElementCursor(element);
|
|
562
|
+
|
|
563
|
+
const { annotation, viewportIdsToRender, newAnnotation } = this.editData;
|
|
564
|
+
|
|
565
|
+
if (newAnnotation) {
|
|
566
|
+
removeAnnotation(annotation.annotationUID);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const enabledElement = getEnabledElement(element);
|
|
570
|
+
const { renderingEngine } = enabledElement;
|
|
571
|
+
|
|
572
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
573
|
+
|
|
574
|
+
this.editData = null;
|
|
575
|
+
this.scissors = null;
|
|
576
|
+
return annotation.annotationUID;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Triggers an annotation modified event.
|
|
581
|
+
*/
|
|
582
|
+
triggerAnnotationModified = (
|
|
583
|
+
annotation: LivewireContourAnnotation,
|
|
584
|
+
enabledElement: Types.IEnabledElement
|
|
585
|
+
): void => {
|
|
586
|
+
const { viewportId, renderingEngineId } = enabledElement;
|
|
587
|
+
const eventType = Events.ANNOTATION_MODIFIED;
|
|
588
|
+
|
|
589
|
+
const eventDetail: AnnotationModifiedEventDetail = {
|
|
590
|
+
annotation,
|
|
591
|
+
viewportId,
|
|
592
|
+
renderingEngineId,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
triggerEvent(eventTarget, eventType, eventDetail);
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
private _activateModify = (element) => {
|
|
599
|
+
state.isInteractingWithTool = true;
|
|
600
|
+
|
|
601
|
+
element.addEventListener(Events.MOUSE_UP, this._endCallback);
|
|
602
|
+
element.addEventListener(Events.MOUSE_DRAG, this._dragCallback);
|
|
603
|
+
element.addEventListener(Events.MOUSE_CLICK, this._endCallback);
|
|
604
|
+
|
|
605
|
+
element.addEventListener(Events.TOUCH_END, this._endCallback);
|
|
606
|
+
element.addEventListener(Events.TOUCH_DRAG, this._dragCallback);
|
|
607
|
+
element.addEventListener(Events.TOUCH_TAP, this._endCallback);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
private _deactivateModify = (element) => {
|
|
611
|
+
state.isInteractingWithTool = false;
|
|
612
|
+
|
|
613
|
+
element.removeEventListener(Events.MOUSE_UP, this._endCallback);
|
|
614
|
+
element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback);
|
|
615
|
+
element.removeEventListener(Events.MOUSE_CLICK, this._endCallback);
|
|
616
|
+
|
|
617
|
+
element.removeEventListener(Events.TOUCH_END, this._endCallback);
|
|
618
|
+
element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback);
|
|
619
|
+
element.removeEventListener(Events.TOUCH_TAP, this._endCallback);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
private _activateDraw = (element) => {
|
|
623
|
+
state.isInteractingWithTool = true;
|
|
624
|
+
|
|
625
|
+
element.addEventListener(Events.MOUSE_MOVE, this._mouseMoveCallback);
|
|
626
|
+
element.addEventListener(Events.MOUSE_DOWN, this._mouseDownCallback);
|
|
627
|
+
element.addEventListener(
|
|
628
|
+
Events.MOUSE_DOUBLE_CLICK,
|
|
629
|
+
this._mouseDownCallback
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
element.addEventListener(Events.TOUCH_TAP, this._mouseDownCallback);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
private _deactivateDraw = (element) => {
|
|
636
|
+
state.isInteractingWithTool = false;
|
|
637
|
+
|
|
638
|
+
element.removeEventListener(Events.MOUSE_MOVE, this._mouseMoveCallback);
|
|
639
|
+
element.removeEventListener(Events.MOUSE_DOWN, this._mouseDownCallback);
|
|
640
|
+
element.removeEventListener(
|
|
641
|
+
Events.MOUSE_DOUBLE_CLICK,
|
|
642
|
+
this._mouseDownCallback
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
element.removeEventListener(Events.TOUCH_TAP, this._mouseDownCallback);
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* it is used to draw the circleROI annotation in each
|
|
650
|
+
* request animation frame. It calculates the updated cached statistics if
|
|
651
|
+
* data is invalidated and cache it.
|
|
652
|
+
*
|
|
653
|
+
* @param enabledElement - The Cornerstone's enabledElement.
|
|
654
|
+
* @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
|
|
655
|
+
*/
|
|
656
|
+
renderAnnotation = (
|
|
657
|
+
enabledElement: Types.IEnabledElement,
|
|
658
|
+
svgDrawingHelper: SVGDrawingHelper
|
|
659
|
+
): boolean => {
|
|
660
|
+
let renderStatus = false;
|
|
661
|
+
const { viewport } = enabledElement;
|
|
662
|
+
const { worldToCanvas } = viewport;
|
|
663
|
+
const { element } = viewport;
|
|
664
|
+
|
|
665
|
+
// If rendering engine has been destroyed while rendering
|
|
666
|
+
if (!viewport.getRenderingEngine()) {
|
|
667
|
+
console.warn('Rendering Engine has been destroyed');
|
|
668
|
+
return renderStatus;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let annotations = getAnnotations(this.getToolName(), element);
|
|
672
|
+
|
|
673
|
+
if (!annotations?.length) {
|
|
674
|
+
return renderStatus;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
annotations = this.filterInteractableAnnotationsForElement(
|
|
678
|
+
element,
|
|
679
|
+
annotations
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
if (!annotations?.length) {
|
|
683
|
+
return renderStatus;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const newAnnotation = this.editData?.newAnnotation;
|
|
687
|
+
const styleSpecifier: StyleSpecifier = {
|
|
688
|
+
toolGroupId: this.toolGroupId,
|
|
689
|
+
toolName: this.getToolName(),
|
|
690
|
+
viewportId: enabledElement.viewport.id,
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Update the annotation that is in editData (being edited)
|
|
694
|
+
this._updateAnnotation(element, this.editData?.currentPath);
|
|
695
|
+
|
|
696
|
+
for (let i = 0; i < annotations.length; i++) {
|
|
697
|
+
const annotation = annotations[i] as LivewireContourAnnotation;
|
|
698
|
+
const { annotationUID, data } = annotation;
|
|
699
|
+
const { handles } = data;
|
|
700
|
+
const { points } = handles;
|
|
701
|
+
|
|
702
|
+
styleSpecifier.annotationUID = annotationUID;
|
|
703
|
+
|
|
704
|
+
const lineWidth = this.getStyle(
|
|
705
|
+
'lineWidth',
|
|
706
|
+
styleSpecifier,
|
|
707
|
+
annotation
|
|
708
|
+
) as number;
|
|
709
|
+
const lineDash = this.getStyle(
|
|
710
|
+
'lineDash',
|
|
711
|
+
styleSpecifier,
|
|
712
|
+
annotation
|
|
713
|
+
) as string;
|
|
714
|
+
const color = this.getStyle(
|
|
715
|
+
'color',
|
|
716
|
+
styleSpecifier,
|
|
717
|
+
annotation
|
|
718
|
+
) as string;
|
|
719
|
+
|
|
720
|
+
const canvasCoordinates = points.map((p) =>
|
|
721
|
+
worldToCanvas(p)
|
|
722
|
+
) as Types.Point2[];
|
|
723
|
+
|
|
724
|
+
if (!isAnnotationVisible(annotationUID)) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Render the first control point only when the annotaion is drawn for the
|
|
729
|
+
// first time to make it easier to know where the user needs to click to
|
|
730
|
+
// to close the ROI.
|
|
731
|
+
if (
|
|
732
|
+
newAnnotation &&
|
|
733
|
+
annotation.annotationUID === this.editData?.annotation?.annotationUID
|
|
734
|
+
) {
|
|
735
|
+
const handleGroupUID = '0';
|
|
736
|
+
drawHandlesSvg(
|
|
737
|
+
svgDrawingHelper,
|
|
738
|
+
annotationUID,
|
|
739
|
+
handleGroupUID,
|
|
740
|
+
[canvasCoordinates[0]],
|
|
741
|
+
{
|
|
742
|
+
color,
|
|
743
|
+
lineDash,
|
|
744
|
+
lineWidth,
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const canvasPolyline = data.polyline.map((worldPoint) =>
|
|
750
|
+
viewport.worldToCanvas(worldPoint)
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
drawPolylineSvg(
|
|
754
|
+
svgDrawingHelper,
|
|
755
|
+
annotationUID,
|
|
756
|
+
'polyline',
|
|
757
|
+
canvasPolyline,
|
|
758
|
+
{
|
|
759
|
+
color,
|
|
760
|
+
lineDash,
|
|
761
|
+
lineWidth,
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
renderStatus = true;
|
|
766
|
+
annotation.invalidated = false;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return renderStatus;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
private _updateAnnotation(
|
|
773
|
+
element: HTMLDivElement,
|
|
774
|
+
livewirePath: LivewirePath
|
|
775
|
+
) {
|
|
776
|
+
if (!this.editData || !livewirePath) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const { pointArray: imagePoints } = livewirePath;
|
|
781
|
+
const worldPolylinePoints: Types.Point3[] = [];
|
|
782
|
+
const { sliceToWorld } = this.editData;
|
|
783
|
+
|
|
784
|
+
for (let i = 0, len = imagePoints.length; i < len; i++) {
|
|
785
|
+
const imagePoint = imagePoints[i];
|
|
786
|
+
const worldPoint = sliceToWorld(imagePoint);
|
|
787
|
+
worldPolylinePoints.push(worldPoint);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (worldPolylinePoints.length > 1) {
|
|
791
|
+
worldPolylinePoints.push([...worldPolylinePoints[0]]);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.editData.annotation.data.polyline = worldPolylinePoints;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
LivewireContourTool.toolName = 'LivewireContour';
|
|
799
|
+
export default LivewireContourTool;
|