@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.
- package/dist/esm/enums/MeasurementType.d.ts +6 -0
- package/dist/esm/enums/MeasurementType.js +7 -0
- package/dist/esm/enums/index.d.ts +1 -0
- package/dist/esm/enums/index.js +1 -0
- package/dist/esm/tools/SculptorTool/CircleSculptCursor.d.ts +8 -10
- package/dist/esm/tools/SculptorTool/CircleSculptCursor.js +33 -133
- package/dist/esm/tools/SculptorTool.d.ts +20 -5
- package/dist/esm/tools/SculptorTool.js +243 -52
- package/dist/esm/tools/annotation/BidirectionalTool.d.ts +0 -2
- package/dist/esm/tools/annotation/BidirectionalTool.js +24 -28
- package/dist/esm/tools/annotation/CircleROITool.d.ts +1 -2
- package/dist/esm/tools/annotation/CircleROITool.js +51 -44
- package/dist/esm/tools/annotation/EllipticalROITool.js +1 -1
- package/dist/esm/tools/annotation/LengthTool.d.ts +0 -2
- package/dist/esm/tools/annotation/LengthTool.js +13 -25
- package/dist/esm/tools/annotation/PlanarFreehandROITool.d.ts +2 -1
- package/dist/esm/tools/annotation/PlanarFreehandROITool.js +70 -68
- package/dist/esm/tools/base/BaseTool.d.ts +4 -2
- package/dist/esm/tools/base/BaseTool.js +38 -11
- package/dist/esm/tools/segmentation/BrushTool.js +9 -0
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +4 -1
- package/dist/esm/types/CalculatorTypes.d.ts +4 -3
- package/dist/esm/types/ISculptToolShape.d.ts +6 -4
- package/dist/esm/utilities/contours/index.d.ts +1 -2
- package/dist/esm/utilities/contours/index.js +1 -2
- package/dist/esm/utilities/getCalibratedUnits.d.ts +2 -0
- package/dist/esm/utilities/getCalibratedUnits.js +32 -63
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -3
- package/dist/esm/utilities/contours/calculatePerimeter.d.ts +0 -2
- 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
|
|
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
|
|
67
|
-
|
|
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
|
|
110
|
-
if (
|
|
111
|
-
|
|
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 {
|
|
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
|
-
|
|
145
|
-
cursorShape.updateToolSize(canvasCoords, viewport, activeAnnotation);
|
|
146
|
-
}
|
|
339
|
+
cursorShape.updateToolSize(canvasCoords, viewport, activeAnnotation);
|
|
147
340
|
}
|
|
148
341
|
triggerAnnotationRenderForViewportIds(this.commonData.viewportIdsToRender);
|
|
149
342
|
}
|
|
150
|
-
|
|
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(
|
|
157
|
-
|
|
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
|
|
614
|
-
const
|
|
615
|
-
const
|
|
616
|
-
const
|
|
617
|
-
const
|
|
618
|
-
const
|
|
619
|
-
const {
|
|
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
|
|
626
|
-
|
|
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,
|
|
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 {
|
|
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,
|
|
475
|
+
this._calculateCachedStats = (annotation, viewport, _renderingEngine, _enabledElement) => {
|
|
477
476
|
if (!this.configuration.calculateStats) {
|
|
478
477
|
return;
|
|
479
478
|
}
|
|
480
|
-
const data = annotation
|
|
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
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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) => {
|