@cornerstonejs/tools 1.16.6 → 1.17.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/OverlayGridTool.d.ts +31 -0
- package/dist/cjs/tools/OverlayGridTool.js +170 -0
- package/dist/cjs/tools/OverlayGridTool.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/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tools/OverlayGridTool.d.ts +31 -0
- package/dist/esm/tools/OverlayGridTool.js +165 -0
- package/dist/esm/tools/OverlayGridTool.js.map +1 -0
- package/dist/esm/tools/index.d.ts +2 -1
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/tools/OverlayGridTool.ts +357 -0
- package/src/tools/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cornerstonejs/tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Cornerstone3D Tools",
|
|
5
5
|
"main": "dist/umd/index.js",
|
|
6
6
|
"types": "dist/esm/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.
|
|
32
|
+
"@cornerstonejs/core": "^1.17.0",
|
|
33
33
|
"lodash.clonedeep": "4.5.0",
|
|
34
34
|
"lodash.get": "^4.4.2"
|
|
35
35
|
},
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"type": "individual",
|
|
53
53
|
"url": "https://ohif.org/donate"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "1981588ef6468e8482973c0bb2048f09bbc6c113"
|
|
56
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
ReferenceLines,
|
|
59
59
|
PaintFillTool,
|
|
60
60
|
ScaleOverlayTool,
|
|
61
|
+
OverlayGridTool,
|
|
61
62
|
} from './tools';
|
|
62
63
|
|
|
63
64
|
import * as Enums from './enums';
|
|
@@ -88,6 +89,7 @@ export {
|
|
|
88
89
|
LengthTool,
|
|
89
90
|
CrosshairsTool,
|
|
90
91
|
ReferenceLinesTool,
|
|
92
|
+
OverlayGridTool,
|
|
91
93
|
ProbeTool,
|
|
92
94
|
RectangleROITool,
|
|
93
95
|
EllipticalROITool,
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { vec3 } from 'gl-matrix';
|
|
2
|
+
import {
|
|
3
|
+
metaData,
|
|
4
|
+
CONSTANTS,
|
|
5
|
+
getRenderingEngine,
|
|
6
|
+
utilities as csUtils,
|
|
7
|
+
} from '@cornerstonejs/core';
|
|
8
|
+
import type { Types } from '@cornerstonejs/core';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
addAnnotation,
|
|
12
|
+
getAnnotations,
|
|
13
|
+
} from '../stateManagement/annotation/annotationState';
|
|
14
|
+
|
|
15
|
+
import { getToolGroup } from '../store/ToolGroupManager';
|
|
16
|
+
|
|
17
|
+
import { drawLine as drawLineSvg } from '../drawingSvg';
|
|
18
|
+
import triggerAnnotationRenderForViewportIds from '../utilities/triggerAnnotationRenderForViewportIds';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
PublicToolProps,
|
|
22
|
+
ToolProps,
|
|
23
|
+
SVGDrawingHelper,
|
|
24
|
+
Annotation,
|
|
25
|
+
} from '../types';
|
|
26
|
+
import { StyleSpecifier } from '../types/AnnotationStyle';
|
|
27
|
+
import AnnotationDisplayTool from './base/AnnotationDisplayTool';
|
|
28
|
+
|
|
29
|
+
const { EPSILON } = CONSTANTS;
|
|
30
|
+
|
|
31
|
+
export interface OverlayGridAnnotation extends Annotation {
|
|
32
|
+
data: {
|
|
33
|
+
viewportData: Map<string, object>;
|
|
34
|
+
pointSets: Array<object>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
class OverlayGridTool extends AnnotationDisplayTool {
|
|
42
|
+
static toolName;
|
|
43
|
+
|
|
44
|
+
public touchDragCallback: any;
|
|
45
|
+
public mouseDragCallback: any;
|
|
46
|
+
_throttledCalculateCachedStats: any;
|
|
47
|
+
isDrawing: boolean;
|
|
48
|
+
isHandleOutsideImage: boolean;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
toolProps: PublicToolProps = {},
|
|
52
|
+
defaultToolProps: ToolProps = {
|
|
53
|
+
supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
54
|
+
configuration: {
|
|
55
|
+
sourceImageIds: [],
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
) {
|
|
59
|
+
super(toolProps, defaultToolProps);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onSetToolEnabled = (): void => {
|
|
63
|
+
this._init();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
onSetToolActive = (): void => {
|
|
67
|
+
this._init();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
_init = (): void => {
|
|
71
|
+
const sourceImageIds = this.configuration.sourceImageIds;
|
|
72
|
+
if (!sourceImageIds?.length) {
|
|
73
|
+
console.warn(
|
|
74
|
+
'OverlayGridTool: No sourceImageIds provided in configuration'
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const imagePlaneModule = metaData.get(
|
|
80
|
+
'imagePlaneModule',
|
|
81
|
+
sourceImageIds[0]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!imagePlaneModule) {
|
|
85
|
+
console.warn(
|
|
86
|
+
'OverlayGridTool: No imagePlaneModule found for sourceImageIds'
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { frameOfReferenceUID } = imagePlaneModule;
|
|
92
|
+
|
|
93
|
+
const viewportsInfo = getToolGroup(this.toolGroupId).viewportsInfo;
|
|
94
|
+
|
|
95
|
+
if (!viewportsInfo?.length) {
|
|
96
|
+
console.warn('OverlayGridTool: No viewports found');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const annotations = getAnnotations(this.getToolName(), frameOfReferenceUID);
|
|
101
|
+
|
|
102
|
+
if (!annotations?.length) {
|
|
103
|
+
const pointSets = sourceImageIds.map((id) => {
|
|
104
|
+
// check if pointSets for the imageId was calculated. If not calculate and store
|
|
105
|
+
return this.calculateImageIdPointSets(id);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const newAnnotation: OverlayGridAnnotation = {
|
|
109
|
+
highlighted: true,
|
|
110
|
+
invalidated: true,
|
|
111
|
+
metadata: {
|
|
112
|
+
toolName: this.getToolName(),
|
|
113
|
+
FrameOfReferenceUID: frameOfReferenceUID,
|
|
114
|
+
referencedImageId: null,
|
|
115
|
+
},
|
|
116
|
+
data: {
|
|
117
|
+
viewportData: new Map(),
|
|
118
|
+
pointSets,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
addAnnotation(newAnnotation, frameOfReferenceUID);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
triggerAnnotationRenderForViewportIds(
|
|
126
|
+
getRenderingEngine(viewportsInfo[0].renderingEngineId),
|
|
127
|
+
viewportsInfo.map(({ viewportId }) => viewportId)
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Calculates the point sets based on the image corners relative to an imageId
|
|
133
|
+
* @param imageId - The imageId to calculate the point sets for
|
|
134
|
+
* @returns
|
|
135
|
+
*/
|
|
136
|
+
calculateImageIdPointSets = (imageId: string) => {
|
|
137
|
+
const {
|
|
138
|
+
imagePositionPatient,
|
|
139
|
+
rows,
|
|
140
|
+
columns,
|
|
141
|
+
rowCosines,
|
|
142
|
+
columnCosines,
|
|
143
|
+
rowPixelSpacing,
|
|
144
|
+
columnPixelSpacing,
|
|
145
|
+
} = metaData.get('imagePlaneModule', imageId);
|
|
146
|
+
|
|
147
|
+
// top left world, top right world, bottom right world, bottom left world
|
|
148
|
+
const topLeft = <Types.Point3>[...imagePositionPatient];
|
|
149
|
+
const topRight = <Types.Point3>[...imagePositionPatient];
|
|
150
|
+
const bottomLeft = <Types.Point3>[...imagePositionPatient];
|
|
151
|
+
const bottomRight = <Types.Point3>[...imagePositionPatient];
|
|
152
|
+
|
|
153
|
+
vec3.scaleAndAdd(
|
|
154
|
+
topRight,
|
|
155
|
+
imagePositionPatient,
|
|
156
|
+
columnCosines,
|
|
157
|
+
columns * columnPixelSpacing
|
|
158
|
+
);
|
|
159
|
+
vec3.scaleAndAdd(
|
|
160
|
+
bottomLeft,
|
|
161
|
+
imagePositionPatient,
|
|
162
|
+
rowCosines,
|
|
163
|
+
rows * rowPixelSpacing
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
vec3.scaleAndAdd(
|
|
167
|
+
bottomRight,
|
|
168
|
+
bottomLeft,
|
|
169
|
+
columnCosines,
|
|
170
|
+
columns * columnPixelSpacing
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// check if the topLeft and bottomLeft line is parallel to the viewUp
|
|
174
|
+
const pointSet1 = [topLeft, bottomLeft, topRight, bottomRight];
|
|
175
|
+
const pointSet2 = [topLeft, topRight, bottomLeft, bottomRight];
|
|
176
|
+
|
|
177
|
+
return { pointSet1, pointSet2 };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* it is used to draw the length annotation in each
|
|
182
|
+
* request animation frame. It calculates the updated cached statistics if
|
|
183
|
+
* data is invalidated and cache it.
|
|
184
|
+
*
|
|
185
|
+
* @param enabledElement - The Cornerstone's enabledElement.
|
|
186
|
+
* @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
|
|
187
|
+
*/
|
|
188
|
+
renderAnnotation = (
|
|
189
|
+
enabledElement: Types.IEnabledElement,
|
|
190
|
+
svgDrawingHelper: SVGDrawingHelper
|
|
191
|
+
): boolean => {
|
|
192
|
+
const sourceImageIds = this.configuration.sourceImageIds;
|
|
193
|
+
|
|
194
|
+
let renderStatus = false;
|
|
195
|
+
if (!sourceImageIds?.length) {
|
|
196
|
+
return renderStatus;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { viewport: targetViewport, FrameOfReferenceUID } = enabledElement;
|
|
200
|
+
const targetImageIds = targetViewport.getImageIds();
|
|
201
|
+
if (targetImageIds.length < 2) {
|
|
202
|
+
return renderStatus;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const annotations = getAnnotations(this.getToolName(), FrameOfReferenceUID);
|
|
206
|
+
if (!annotations?.length) {
|
|
207
|
+
return renderStatus;
|
|
208
|
+
}
|
|
209
|
+
const annotation = annotations[0];
|
|
210
|
+
const { annotationUID } = annotation;
|
|
211
|
+
|
|
212
|
+
const { focalPoint, viewPlaneNormal } = targetViewport.getCamera();
|
|
213
|
+
|
|
214
|
+
const styleSpecifier: StyleSpecifier = {
|
|
215
|
+
toolGroupId: this.toolGroupId,
|
|
216
|
+
toolName: this.getToolName(),
|
|
217
|
+
viewportId: enabledElement.viewport.id,
|
|
218
|
+
};
|
|
219
|
+
const imageIdNormal = <Types.Point3>(
|
|
220
|
+
this.getImageIdNormal(sourceImageIds[0])
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (this.isParallel(viewPlaneNormal, imageIdNormal)) {
|
|
224
|
+
// If the source and target viewports are parallel, we don't need to render
|
|
225
|
+
return renderStatus;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const targetViewportPlane = csUtils.planar.planeEquation(
|
|
229
|
+
viewPlaneNormal,
|
|
230
|
+
focalPoint
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const pointSets = annotation.data.pointSets;
|
|
234
|
+
const viewportData = annotation.data.viewportData;
|
|
235
|
+
for (let i = 0; i < sourceImageIds.length; i++) {
|
|
236
|
+
// check if pointSets for the imageId was calculated. If not calculate and store
|
|
237
|
+
const { pointSet1, pointSet2 } = pointSets[i];
|
|
238
|
+
|
|
239
|
+
const targetData =
|
|
240
|
+
viewportData.get(targetViewport.id) ||
|
|
241
|
+
this.initializeViewportData(viewportData, targetViewport.id);
|
|
242
|
+
|
|
243
|
+
// check if pointSetToUse was calculated. If not calculate and store
|
|
244
|
+
if (!targetData.pointSetsToUse[i]) {
|
|
245
|
+
let pointSetToUse = pointSet1;
|
|
246
|
+
|
|
247
|
+
let topBottomVec = vec3.subtract(
|
|
248
|
+
vec3.create(),
|
|
249
|
+
pointSet1[0],
|
|
250
|
+
pointSet1[1]
|
|
251
|
+
);
|
|
252
|
+
topBottomVec = vec3.normalize(
|
|
253
|
+
vec3.create(),
|
|
254
|
+
topBottomVec
|
|
255
|
+
) as Types.Point3;
|
|
256
|
+
|
|
257
|
+
// check if it is perpendicular to the viewPlaneNormal which means
|
|
258
|
+
// the line does not intersect the viewPlaneNormal
|
|
259
|
+
if (this.isPerpendicular(topBottomVec, viewPlaneNormal)) {
|
|
260
|
+
// 'use pointSet2';
|
|
261
|
+
pointSetToUse = pointSet2;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
targetData.pointSetsToUse[i] = pointSetToUse;
|
|
265
|
+
|
|
266
|
+
targetData.lineStartsWorld[i] = csUtils.planar.linePlaneIntersection(
|
|
267
|
+
pointSetToUse[0],
|
|
268
|
+
pointSetToUse[1],
|
|
269
|
+
targetViewportPlane
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
targetData.lineEndsWorld[i] = csUtils.planar.linePlaneIntersection(
|
|
273
|
+
pointSetToUse[2],
|
|
274
|
+
pointSetToUse[3],
|
|
275
|
+
targetViewportPlane
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const lineStartWorld = targetData.lineStartsWorld[i];
|
|
280
|
+
const lineEndWorld = targetData.lineEndsWorld[i];
|
|
281
|
+
|
|
282
|
+
styleSpecifier.annotationUID = annotationUID;
|
|
283
|
+
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
|
284
|
+
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
|
285
|
+
const color = this.getStyle('color', styleSpecifier, annotation);
|
|
286
|
+
const shadow = this.getStyle('shadow', styleSpecifier, annotation);
|
|
287
|
+
|
|
288
|
+
const canvasCoordinates = [lineStartWorld, lineEndWorld].map((world) =>
|
|
289
|
+
targetViewport.worldToCanvas(world)
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const dataId = `${annotationUID}-line`;
|
|
293
|
+
const lineUID = `${i}`;
|
|
294
|
+
drawLineSvg(
|
|
295
|
+
svgDrawingHelper,
|
|
296
|
+
annotationUID,
|
|
297
|
+
lineUID,
|
|
298
|
+
canvasCoordinates[0],
|
|
299
|
+
canvasCoordinates[1],
|
|
300
|
+
{
|
|
301
|
+
color,
|
|
302
|
+
width: lineWidth,
|
|
303
|
+
lineDash,
|
|
304
|
+
shadow,
|
|
305
|
+
},
|
|
306
|
+
dataId
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
renderStatus = true;
|
|
311
|
+
|
|
312
|
+
return renderStatus;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
private initializeViewportData = (viewportData, id) => {
|
|
316
|
+
viewportData.set(id, {
|
|
317
|
+
pointSetsToUse: [],
|
|
318
|
+
lineStartsWorld: [],
|
|
319
|
+
lineEndsWorld: [],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return viewportData.get(id);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
private isPerpendicular = (
|
|
326
|
+
vec1: Types.Point3,
|
|
327
|
+
vec2: Types.Point3
|
|
328
|
+
): boolean => {
|
|
329
|
+
const dot = vec3.dot(vec1, vec2);
|
|
330
|
+
return Math.abs(dot) < EPSILON;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
private isParallel(vec1: Types.Point3, vec2: Types.Point3): boolean {
|
|
334
|
+
return Math.abs(vec3.dot(vec1, vec2)) > 1 - EPSILON;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private getImageIdNormal(imageId: string): vec3 {
|
|
338
|
+
const { imageOrientationPatient } = metaData.get(
|
|
339
|
+
'imagePlaneModule',
|
|
340
|
+
imageId
|
|
341
|
+
);
|
|
342
|
+
const rowCosineVec = vec3.fromValues(
|
|
343
|
+
imageOrientationPatient[0],
|
|
344
|
+
imageOrientationPatient[1],
|
|
345
|
+
imageOrientationPatient[2]
|
|
346
|
+
);
|
|
347
|
+
const colCosineVec = vec3.fromValues(
|
|
348
|
+
imageOrientationPatient[3],
|
|
349
|
+
imageOrientationPatient[4],
|
|
350
|
+
imageOrientationPatient[5]
|
|
351
|
+
);
|
|
352
|
+
return vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
OverlayGridTool.toolName = 'OverlayGrid';
|
|
357
|
+
export default OverlayGridTool;
|
package/src/tools/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import MIPJumpToClickTool from './MIPJumpToClickTool';
|
|
|
11
11
|
import CrosshairsTool from './CrosshairsTool';
|
|
12
12
|
import MagnifyTool from './MagnifyTool';
|
|
13
13
|
import ReferenceLinesTool from './ReferenceLinesTool';
|
|
14
|
+
import OverlayGridTool from './OverlayGridTool';
|
|
14
15
|
//
|
|
15
16
|
import BidirectionalTool from './annotation/BidirectionalTool';
|
|
16
17
|
import LengthTool from './annotation/LengthTool';
|
|
@@ -58,6 +59,7 @@ export {
|
|
|
58
59
|
// Annotation Tools
|
|
59
60
|
CrosshairsTool,
|
|
60
61
|
ReferenceLinesTool,
|
|
62
|
+
OverlayGridTool,
|
|
61
63
|
BidirectionalTool,
|
|
62
64
|
LengthTool,
|
|
63
65
|
ProbeTool,
|