@cornerstonejs/tools 4.4.1 → 4.5.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/esm/tools/annotation/EllipticalROITool.js +74 -63
- package/dist/esm/tools/segmentation/BrushTool.d.ts +1 -0
- package/dist/esm/tools/segmentation/BrushTool.js +44 -3
- package/dist/esm/tools/segmentation/strategies/__tests__/fillCircle.spec.d.ts +1 -0
- package/dist/esm/tools/segmentation/strategies/__tests__/fillCircle.spec.js +27 -0
- package/dist/esm/tools/segmentation/strategies/fillCircle.d.ts +7 -2
- package/dist/esm/tools/segmentation/strategies/fillCircle.js +125 -11
- package/dist/esm/tools/segmentation/strategies/fillSphere.js +67 -3
- package/dist/esm/types/LabelmapToolOperationData.d.ts +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnnotationTool } from '../base';
|
|
2
|
-
import { getEnabledElement, VolumeViewport, utilities as csUtils, getEnabledElementByViewportId, } from '@cornerstonejs/core';
|
|
3
|
-
import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits';
|
|
2
|
+
import { getEnabledElement, VolumeViewport, utilities as csUtils, getEnabledElementByViewportId, EPSILON, } from '@cornerstonejs/core';
|
|
3
|
+
import { getCalibratedAspect, getCalibratedLengthUnitsAndScale, } from '../../utilities/getCalibratedUnits';
|
|
4
4
|
import throttle from '../../utilities/throttle';
|
|
5
5
|
import { addAnnotation, getAnnotations, removeAnnotation, } from '../../stateManagement/annotation/annotationState';
|
|
6
6
|
import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
|
|
@@ -535,68 +535,79 @@ class EllipticalROITool extends AnnotationTool {
|
|
|
535
535
|
pos1Index[0] = Math.floor(pos1Index[0]);
|
|
536
536
|
pos1Index[1] = Math.floor(pos1Index[1]);
|
|
537
537
|
pos1Index[2] = Math.floor(pos1Index[2]);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
scale;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
538
|
+
const pos2Index = transformWorldToIndex(imageData, worldPos2);
|
|
539
|
+
pos2Index[0] = Math.floor(pos2Index[0]);
|
|
540
|
+
pos2Index[1] = Math.floor(pos2Index[1]);
|
|
541
|
+
pos2Index[2] = Math.floor(pos2Index[2]);
|
|
542
|
+
if (this._isInsideVolume(pos1Index, pos2Index, dimensions)) {
|
|
543
|
+
const iMin = Math.min(pos1Index[0], pos2Index[0]);
|
|
544
|
+
const iMax = Math.max(pos1Index[0], pos2Index[0]);
|
|
545
|
+
const jMin = Math.min(pos1Index[1], pos2Index[1]);
|
|
546
|
+
const jMax = Math.max(pos1Index[1], pos2Index[1]);
|
|
547
|
+
const kMin = Math.min(pos1Index[2], pos2Index[2]);
|
|
548
|
+
const kMax = Math.max(pos1Index[2], pos2Index[2]);
|
|
549
|
+
const boundsIJK = [
|
|
550
|
+
[iMin, iMax],
|
|
551
|
+
[jMin, jMax],
|
|
552
|
+
[kMin, kMax],
|
|
553
|
+
];
|
|
554
|
+
const center = [
|
|
555
|
+
(topLeftWorld[0] + bottomRightWorld[0]) / 2,
|
|
556
|
+
(topLeftWorld[1] + bottomRightWorld[1]) / 2,
|
|
557
|
+
(topLeftWorld[2] + bottomRightWorld[2]) / 2,
|
|
558
|
+
];
|
|
559
|
+
const xRadius = Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2;
|
|
560
|
+
const yRadius = Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2;
|
|
561
|
+
const zRadius = Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2;
|
|
562
|
+
const ellipseObj = {
|
|
563
|
+
center,
|
|
564
|
+
xRadius: xRadius < EPSILON / 2 ? 0 : xRadius,
|
|
565
|
+
yRadius: yRadius < EPSILON / 2 ? 0 : yRadius,
|
|
566
|
+
zRadius: zRadius < EPSILON / 2 ? 0 : zRadius,
|
|
567
|
+
};
|
|
568
|
+
const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints(viewPlaneNormal, viewUp, worldPos1, worldPos2);
|
|
569
|
+
const isEmptyArea = worldWidth === 0 && worldHeight === 0;
|
|
570
|
+
const handles = [pos1Index, pos2Index];
|
|
571
|
+
const { scale, unit, areaUnit } = getCalibratedLengthUnitsAndScale(image, handles);
|
|
572
|
+
const aspect = getCalibratedAspect(image);
|
|
573
|
+
const area = Math.abs(Math.PI *
|
|
574
|
+
(worldWidth / scale / 2) *
|
|
575
|
+
(worldHeight / aspect / scale / 2));
|
|
576
|
+
const pixelUnitsOptions = {
|
|
577
|
+
isPreScaled: isViewportPreScaled(viewport, targetId),
|
|
578
|
+
isSuvScaled: this.isSuvScaled(viewport, targetId, annotation.metadata.referencedImageId),
|
|
579
|
+
};
|
|
580
|
+
const modalityUnit = getPixelValueUnits(metadata.Modality, annotation.metadata.referencedImageId, pixelUnitsOptions);
|
|
581
|
+
let pointsInShape;
|
|
582
|
+
if (voxelManager) {
|
|
583
|
+
pointsInShape = voxelManager.forEach(this.configuration.statsCalculator.statsCallback, {
|
|
584
|
+
isInObject: (pointLPS) => pointInEllipse(ellipseObj, pointLPS, { fast: true }),
|
|
585
|
+
boundsIJK,
|
|
586
|
+
imageData,
|
|
587
|
+
returnPoints: this.configuration.storePointData,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
const stats = this.configuration.statsCalculator.getStatistics();
|
|
591
|
+
cachedStats[targetId] = {
|
|
592
|
+
Modality: metadata.Modality,
|
|
593
|
+
area,
|
|
594
|
+
mean: stats.mean?.value,
|
|
595
|
+
max: stats.max?.value,
|
|
596
|
+
min: stats.min?.value,
|
|
597
|
+
stdDev: stats.stdDev?.value,
|
|
598
|
+
statsArray: stats.array,
|
|
599
|
+
pointsInShape,
|
|
600
|
+
isEmptyArea,
|
|
601
|
+
areaUnit,
|
|
602
|
+
modalityUnit,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
this.isHandleOutsideImage = true;
|
|
607
|
+
cachedStats[targetId] = {
|
|
608
|
+
Modality: metadata.Modality,
|
|
609
|
+
};
|
|
585
610
|
}
|
|
586
|
-
const stats = this.configuration.statsCalculator.getStatistics();
|
|
587
|
-
cachedStats[targetId] = {
|
|
588
|
-
Modality: metadata.Modality,
|
|
589
|
-
area,
|
|
590
|
-
mean: stats.mean?.value,
|
|
591
|
-
max: stats.max?.value,
|
|
592
|
-
min: stats.min?.value,
|
|
593
|
-
stdDev: stats.stdDev?.value,
|
|
594
|
-
statsArray: stats.array,
|
|
595
|
-
pointsInShape,
|
|
596
|
-
isEmptyArea,
|
|
597
|
-
areaUnit,
|
|
598
|
-
modalityUnit,
|
|
599
|
-
};
|
|
600
611
|
}
|
|
601
612
|
const invalidated = annotation.invalidated;
|
|
602
613
|
annotation.invalidated = false;
|
|
@@ -3,6 +3,7 @@ import type { PublicToolProps, ToolProps, EventTypes, SVGDrawingHelper } from '.
|
|
|
3
3
|
import LabelmapBaseTool from './LabelmapBaseTool';
|
|
4
4
|
declare class BrushTool extends LabelmapBaseTool {
|
|
5
5
|
static toolName: any;
|
|
6
|
+
private _lastDragInfo;
|
|
6
7
|
constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
|
|
7
8
|
onSetToolPassive: (evt: any) => void;
|
|
8
9
|
onSetToolEnabled: () => void;
|
|
@@ -80,6 +80,7 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
80
80
|
},
|
|
81
81
|
}) {
|
|
82
82
|
super(toolProps, defaultToolProps);
|
|
83
|
+
this._lastDragInfo = null;
|
|
83
84
|
this.onSetToolPassive = (evt) => {
|
|
84
85
|
this.disableCursor();
|
|
85
86
|
};
|
|
@@ -91,14 +92,24 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
91
92
|
};
|
|
92
93
|
this.preMouseDownCallback = (evt) => {
|
|
93
94
|
const eventData = evt.detail;
|
|
94
|
-
const { element } = eventData;
|
|
95
|
+
const { element, currentPoints } = eventData;
|
|
95
96
|
const enabledElement = getEnabledElement(element);
|
|
97
|
+
const { viewport } = enabledElement;
|
|
96
98
|
this._editData = this.createEditData(element);
|
|
97
99
|
this._activateDraw(element);
|
|
98
100
|
hideElementCursor(element);
|
|
99
101
|
evt.preventDefault();
|
|
100
102
|
this._previewData.isDrag = false;
|
|
101
103
|
this._previewData.timerStart = Date.now();
|
|
104
|
+
const canvasPoint = vec2.clone(currentPoints.canvas);
|
|
105
|
+
const worldPoint = viewport.canvasToWorld([
|
|
106
|
+
canvasPoint[0],
|
|
107
|
+
canvasPoint[1],
|
|
108
|
+
]);
|
|
109
|
+
this._lastDragInfo = {
|
|
110
|
+
canvas: canvasPoint,
|
|
111
|
+
world: vec3.clone(worldPoint),
|
|
112
|
+
};
|
|
102
113
|
const hoverData = this._hoverData || this.createHoverData(element);
|
|
103
114
|
triggerAnnotationRenderForViewportUIDs(hoverData.viewportIdsToRender);
|
|
104
115
|
const operationData = this.getOperationData(element);
|
|
@@ -173,6 +184,7 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
173
184
|
const eventData = evt.detail;
|
|
174
185
|
const { element, currentPoints } = eventData;
|
|
175
186
|
const enabledElement = getEnabledElement(element);
|
|
187
|
+
const { viewport } = enabledElement;
|
|
176
188
|
this.updateCursor(evt);
|
|
177
189
|
const { viewportIdsToRender } = this._hoverData;
|
|
178
190
|
triggerAnnotationRenderForViewportUIDs(viewportIdsToRender);
|
|
@@ -187,11 +199,39 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
187
199
|
window.clearTimeout(this._previewData.timer);
|
|
188
200
|
this._previewData.timer = null;
|
|
189
201
|
}
|
|
190
|
-
|
|
202
|
+
if (!this._lastDragInfo) {
|
|
203
|
+
const startCanvas = this._previewData.startPoint;
|
|
204
|
+
const startWorld = viewport.canvasToWorld([
|
|
205
|
+
startCanvas[0],
|
|
206
|
+
startCanvas[1],
|
|
207
|
+
]);
|
|
208
|
+
this._lastDragInfo = {
|
|
209
|
+
canvas: vec2.clone(startCanvas),
|
|
210
|
+
world: vec3.clone(startWorld),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const currentCanvas = currentPoints.canvas;
|
|
214
|
+
const currentWorld = viewport.canvasToWorld([
|
|
215
|
+
currentCanvas[0],
|
|
216
|
+
currentCanvas[1],
|
|
217
|
+
]);
|
|
218
|
+
this._hoverData = this.createHoverData(element, currentCanvas);
|
|
219
|
+
this._calculateCursor(element, currentCanvas);
|
|
220
|
+
const operationData = this.getOperationData(element);
|
|
221
|
+
operationData.strokePointsWorld = [
|
|
222
|
+
vec3.clone(this._lastDragInfo.world),
|
|
223
|
+
vec3.clone(currentWorld),
|
|
224
|
+
];
|
|
225
|
+
this._previewData.preview = this.applyActiveStrategy(enabledElement, operationData);
|
|
226
|
+
const currentCanvasClone = vec2.clone(currentCanvas);
|
|
227
|
+
this._lastDragInfo = {
|
|
228
|
+
canvas: currentCanvasClone,
|
|
229
|
+
world: vec3.clone(currentWorld),
|
|
230
|
+
};
|
|
191
231
|
this._previewData.element = element;
|
|
192
232
|
this._previewData.timerStart = Date.now() + dragTimeMs;
|
|
193
233
|
this._previewData.isDrag = true;
|
|
194
|
-
this._previewData.startPoint =
|
|
234
|
+
this._previewData.startPoint = currentCanvasClone;
|
|
195
235
|
};
|
|
196
236
|
this._endCallback = (evt) => {
|
|
197
237
|
const eventData = evt.detail;
|
|
@@ -206,6 +246,7 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
206
246
|
resetElementCursor(element);
|
|
207
247
|
this.updateCursor(evt);
|
|
208
248
|
this._editData = null;
|
|
249
|
+
this._lastDragInfo = null;
|
|
209
250
|
this.applyActiveStrategyCallback(enabledElement, operationData, StrategyCallbacks.OnInteractionEnd);
|
|
210
251
|
if (!this._previewData.isDrag) {
|
|
211
252
|
this.acceptPreview(element);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createPointInEllipse } from '../fillCircle';
|
|
2
|
+
describe('createPointInEllipse', () => {
|
|
3
|
+
const corners = [
|
|
4
|
+
[-1, 1, 0],
|
|
5
|
+
[1, -1, 0],
|
|
6
|
+
[-1, -1, 0],
|
|
7
|
+
[1, 1, 0],
|
|
8
|
+
];
|
|
9
|
+
it('detects points inside the base circle', () => {
|
|
10
|
+
const predicate = createPointInEllipse(corners);
|
|
11
|
+
expect(predicate([0, 0, 0])).toBe(true);
|
|
12
|
+
expect(predicate([0.5, 0.5, 0])).toBe(true);
|
|
13
|
+
expect(predicate([1.2, 0, 0])).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
it('covers interpolated stroke segments', () => {
|
|
16
|
+
const predicate = createPointInEllipse(corners, {
|
|
17
|
+
strokePointsWorld: [
|
|
18
|
+
[-2, 0, 0],
|
|
19
|
+
[2, 0, 0],
|
|
20
|
+
],
|
|
21
|
+
radius: 1,
|
|
22
|
+
});
|
|
23
|
+
expect(predicate([0, 0, 0])).toBe(true);
|
|
24
|
+
expect(predicate([1.5, 0, 0])).toBe(true);
|
|
25
|
+
expect(predicate([3.2, 0, 0])).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import type vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
|
2
3
|
import BrushStrategy from './BrushStrategy';
|
|
3
4
|
import type { CanvasCoordinates } from '../../../types';
|
|
4
5
|
export declare function getEllipseCornersFromCanvasCoordinates(canvasCoordinates: CanvasCoordinates): Array<Types.Point2>;
|
|
5
|
-
declare function createPointInEllipse(cornersInWorld?: Types.Point3[]
|
|
6
|
+
declare function createPointInEllipse(cornersInWorld?: Types.Point3[], options?: {
|
|
7
|
+
strokePointsWorld?: Types.Point3[];
|
|
8
|
+
segmentationImageData?: vtkImageData;
|
|
9
|
+
radius?: number;
|
|
10
|
+
}): (pointLPS: Types.Point3 | null, pointIJK?: Types.Point3) => boolean;
|
|
6
11
|
declare const CIRCLE_STRATEGY: BrushStrategy;
|
|
7
12
|
declare const CIRCLE_THRESHOLD_STRATEGY: BrushStrategy;
|
|
8
13
|
declare const fillInsideCircle: (enabledElement: any, operationData: any) => unknown;
|
|
9
14
|
declare const thresholdInsideCircle: (enabledElement: any, operationData: any) => unknown;
|
|
10
15
|
export declare function fillOutsideCircle(): void;
|
|
11
|
-
export { CIRCLE_STRATEGY, CIRCLE_THRESHOLD_STRATEGY, fillInsideCircle, thresholdInsideCircle, createPointInEllipse as createEllipseInPoint, };
|
|
16
|
+
export { CIRCLE_STRATEGY, CIRCLE_THRESHOLD_STRATEGY, fillInsideCircle, thresholdInsideCircle, createPointInEllipse, createPointInEllipse as createEllipseInPoint, };
|
|
@@ -5,7 +5,7 @@ import BrushStrategy from './BrushStrategy';
|
|
|
5
5
|
import { StrategyCallbacks } from '../../../enums';
|
|
6
6
|
import compositions from './compositions';
|
|
7
7
|
import { pointInSphere } from '../../../utilities/math/sphere';
|
|
8
|
-
const { transformWorldToIndex, isEqual } = csUtils;
|
|
8
|
+
const { transformWorldToIndex, transformIndexToWorld, isEqual } = csUtils;
|
|
9
9
|
export function getEllipseCornersFromCanvasCoordinates(canvasCoordinates) {
|
|
10
10
|
const [bottom, top, left, right] = canvasCoordinates;
|
|
11
11
|
const topLeft = [left[0], top[1]];
|
|
@@ -14,9 +14,82 @@ export function getEllipseCornersFromCanvasCoordinates(canvasCoordinates) {
|
|
|
14
14
|
const topRight = [right[0], top[1]];
|
|
15
15
|
return [topLeft, bottomRight, bottomLeft, topRight];
|
|
16
16
|
}
|
|
17
|
+
function createCircleCornersForCenter(center, viewUp, viewRight, radius) {
|
|
18
|
+
const centerVec = vec3.fromValues(center[0], center[1], center[2]);
|
|
19
|
+
const top = vec3.create();
|
|
20
|
+
vec3.scaleAndAdd(top, centerVec, viewUp, radius);
|
|
21
|
+
const bottom = vec3.create();
|
|
22
|
+
vec3.scaleAndAdd(bottom, centerVec, viewUp, -radius);
|
|
23
|
+
const right = vec3.create();
|
|
24
|
+
vec3.scaleAndAdd(right, centerVec, viewRight, radius);
|
|
25
|
+
const left = vec3.create();
|
|
26
|
+
vec3.scaleAndAdd(left, centerVec, viewRight, -radius);
|
|
27
|
+
return [
|
|
28
|
+
bottom,
|
|
29
|
+
top,
|
|
30
|
+
left,
|
|
31
|
+
right,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
function createStrokePredicate(centers, radius) {
|
|
35
|
+
if (!centers.length || radius <= 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const radiusSquared = radius * radius;
|
|
39
|
+
const centerVecs = centers.map((point) => [point[0], point[1], point[2]]);
|
|
40
|
+
const segments = [];
|
|
41
|
+
for (let i = 1; i < centerVecs.length; i++) {
|
|
42
|
+
const start = centerVecs[i - 1];
|
|
43
|
+
const end = centerVecs[i];
|
|
44
|
+
const dx = end[0] - start[0];
|
|
45
|
+
const dy = end[1] - start[1];
|
|
46
|
+
const dz = end[2] - start[2];
|
|
47
|
+
const lengthSquared = dx * dx + dy * dy + dz * dz;
|
|
48
|
+
segments.push({ start, vector: [dx, dy, dz], lengthSquared });
|
|
49
|
+
}
|
|
50
|
+
return (worldPoint) => {
|
|
51
|
+
if (!worldPoint) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
for (const centerVec of centerVecs) {
|
|
55
|
+
const dx = worldPoint[0] - centerVec[0];
|
|
56
|
+
const dy = worldPoint[1] - centerVec[1];
|
|
57
|
+
const dz = worldPoint[2] - centerVec[2];
|
|
58
|
+
if (dx * dx + dy * dy + dz * dz <= radiusSquared) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const { start, vector, lengthSquared } of segments) {
|
|
63
|
+
if (lengthSquared === 0) {
|
|
64
|
+
const dx = worldPoint[0] - start[0];
|
|
65
|
+
const dy = worldPoint[1] - start[1];
|
|
66
|
+
const dz = worldPoint[2] - start[2];
|
|
67
|
+
if (dx * dx + dy * dy + dz * dz <= radiusSquared) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const dx = worldPoint[0] - start[0];
|
|
73
|
+
const dy = worldPoint[1] - start[1];
|
|
74
|
+
const dz = worldPoint[2] - start[2];
|
|
75
|
+
const dot = dx * vector[0] + dy * vector[1] + dz * vector[2];
|
|
76
|
+
const t = Math.max(0, Math.min(1, dot / lengthSquared));
|
|
77
|
+
const projX = start[0] + vector[0] * t;
|
|
78
|
+
const projY = start[1] + vector[1] * t;
|
|
79
|
+
const projZ = start[2] + vector[2] * t;
|
|
80
|
+
const distX = worldPoint[0] - projX;
|
|
81
|
+
const distY = worldPoint[1] - projY;
|
|
82
|
+
const distZ = worldPoint[2] - projZ;
|
|
83
|
+
if (distX * distX + distY * distY + distZ * distZ <= radiusSquared) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
17
90
|
const initializeCircle = {
|
|
18
91
|
[StrategyCallbacks.Initialize]: (operationData) => {
|
|
19
|
-
const { points, viewport, segmentationImageData, } = operationData;
|
|
92
|
+
const { points, viewport, segmentationImageData, viewUp, viewPlaneNormal, } = operationData;
|
|
20
93
|
if (!points) {
|
|
21
94
|
return;
|
|
22
95
|
}
|
|
@@ -30,18 +103,35 @@ const initializeCircle = {
|
|
|
30
103
|
}
|
|
31
104
|
operationData.centerWorld = center;
|
|
32
105
|
operationData.centerIJK = transformWorldToIndex(segmentationImageData, center);
|
|
106
|
+
const brushRadius = points.length >= 2 ? vec3.distance(points[0], points[1]) / 2 : 0;
|
|
33
107
|
const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
|
|
34
108
|
const corners = getEllipseCornersFromCanvasCoordinates(canvasCoordinates);
|
|
35
109
|
const cornersInWorld = corners.map((corner) => viewport.canvasToWorld(corner));
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
110
|
+
const normalizedViewUp = vec3.fromValues(viewUp[0], viewUp[1], viewUp[2]);
|
|
111
|
+
vec3.normalize(normalizedViewUp, normalizedViewUp);
|
|
112
|
+
const normalizedPlaneNormal = vec3.fromValues(viewPlaneNormal[0], viewPlaneNormal[1], viewPlaneNormal[2]);
|
|
113
|
+
vec3.normalize(normalizedPlaneNormal, normalizedPlaneNormal);
|
|
114
|
+
const viewRight = vec3.create();
|
|
115
|
+
vec3.cross(viewRight, normalizedViewUp, normalizedPlaneNormal);
|
|
116
|
+
vec3.normalize(viewRight, viewRight);
|
|
117
|
+
const strokeCentersSource = operationData.strokePointsWorld &&
|
|
118
|
+
operationData.strokePointsWorld.length > 0
|
|
119
|
+
? operationData.strokePointsWorld
|
|
120
|
+
: [operationData.centerWorld];
|
|
121
|
+
const strokeCenters = strokeCentersSource.map((point) => vec3.clone(point));
|
|
122
|
+
const strokeCornersWorld = strokeCenters.flatMap((centerPoint) => createCircleCornersForCenter(centerPoint, normalizedViewUp, viewRight, brushRadius));
|
|
123
|
+
const circleCornersIJK = strokeCornersWorld.map((world) => transformWorldToIndex(segmentationImageData, world));
|
|
39
124
|
const boundsIJK = getBoundingBoxAroundShapeIJK(circleCornersIJK, segmentationImageData.getDimensions());
|
|
40
|
-
operationData.
|
|
125
|
+
operationData.strokePointsWorld = strokeCenters;
|
|
126
|
+
operationData.isInObject = createPointInEllipse(cornersInWorld, {
|
|
127
|
+
strokePointsWorld: strokeCenters,
|
|
128
|
+
segmentationImageData,
|
|
129
|
+
radius: brushRadius,
|
|
130
|
+
});
|
|
41
131
|
operationData.isInObjectBoundsIJK = boundsIJK;
|
|
42
132
|
},
|
|
43
133
|
};
|
|
44
|
-
function createPointInEllipse(cornersInWorld = []) {
|
|
134
|
+
function createPointInEllipse(cornersInWorld = [], options = {}) {
|
|
45
135
|
if (!cornersInWorld || cornersInWorld.length !== 4) {
|
|
46
136
|
throw new Error('createPointInEllipse: cornersInWorld must have 4 points');
|
|
47
137
|
}
|
|
@@ -60,6 +150,8 @@ function createPointInEllipse(cornersInWorld = []) {
|
|
|
60
150
|
const normal = vec3.create();
|
|
61
151
|
vec3.cross(normal, majorAxisVec, minorAxisVec);
|
|
62
152
|
vec3.normalize(normal, normal);
|
|
153
|
+
const radiusForStroke = options.radius ?? Math.max(xRadius, yRadius);
|
|
154
|
+
const strokePredicate = createStrokePredicate(options.strokePointsWorld || [], radiusForStroke);
|
|
63
155
|
if (isEqual(xRadius, yRadius)) {
|
|
64
156
|
const radius = xRadius;
|
|
65
157
|
const sphereObj = {
|
|
@@ -67,11 +159,33 @@ function createPointInEllipse(cornersInWorld = []) {
|
|
|
67
159
|
radius,
|
|
68
160
|
radius2: radius * radius,
|
|
69
161
|
};
|
|
70
|
-
return (pointLPS) =>
|
|
162
|
+
return (pointLPS, pointIJK) => {
|
|
163
|
+
let worldPoint = pointLPS;
|
|
164
|
+
if (!worldPoint && pointIJK && options.segmentationImageData) {
|
|
165
|
+
worldPoint = transformIndexToWorld(options.segmentationImageData, pointIJK);
|
|
166
|
+
}
|
|
167
|
+
if (!worldPoint) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (strokePredicate?.(worldPoint)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return pointInSphere(sphereObj, worldPoint);
|
|
174
|
+
};
|
|
71
175
|
}
|
|
72
|
-
return (pointLPS) => {
|
|
176
|
+
return (pointLPS, pointIJK) => {
|
|
177
|
+
let worldPoint = pointLPS;
|
|
178
|
+
if (!worldPoint && pointIJK && options.segmentationImageData) {
|
|
179
|
+
worldPoint = transformIndexToWorld(options.segmentationImageData, pointIJK);
|
|
180
|
+
}
|
|
181
|
+
if (!worldPoint) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (strokePredicate?.(worldPoint)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
73
187
|
const pointVec = vec3.create();
|
|
74
|
-
vec3.subtract(pointVec,
|
|
188
|
+
vec3.subtract(pointVec, worldPoint, center);
|
|
75
189
|
const distToPlane = vec3.dot(pointVec, normal);
|
|
76
190
|
const proj = vec3.create();
|
|
77
191
|
vec3.scaleAndAdd(proj, pointVec, normal, -distToPlane);
|
|
@@ -91,4 +205,4 @@ const thresholdInsideCircle = CIRCLE_THRESHOLD_STRATEGY.strategyFunction;
|
|
|
91
205
|
export function fillOutsideCircle() {
|
|
92
206
|
throw new Error('Not yet implemented');
|
|
93
207
|
}
|
|
94
|
-
export { CIRCLE_STRATEGY, CIRCLE_THRESHOLD_STRATEGY, fillInsideCircle, thresholdInsideCircle, createPointInEllipse as createEllipseInPoint, };
|
|
208
|
+
export { CIRCLE_STRATEGY, CIRCLE_THRESHOLD_STRATEGY, fillInsideCircle, thresholdInsideCircle, createPointInEllipse, createPointInEllipse as createEllipseInPoint, };
|
|
@@ -22,12 +22,76 @@ const sphereComposition = {
|
|
|
22
22
|
}
|
|
23
23
|
operationData.centerWorld = center;
|
|
24
24
|
operationData.centerIJK = transformWorldToIndex(segmentationImageData, center);
|
|
25
|
-
const
|
|
25
|
+
const baseExtent = getSphereBoundsInfoFromViewport(points.slice(0, 2), segmentationImageData, viewport);
|
|
26
26
|
const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
|
|
27
27
|
const corners = getEllipseCornersFromCanvasCoordinates(canvasCoordinates);
|
|
28
28
|
const cornersInWorld = corners.map((corner) => viewport.canvasToWorld(corner));
|
|
29
|
-
|
|
30
|
-
operationData.
|
|
29
|
+
const strokeRadius = points.length >= 2 ? vec3.distance(points[0], points[1]) / 2 : undefined;
|
|
30
|
+
const strokeCenters = operationData.strokePointsWorld &&
|
|
31
|
+
operationData.strokePointsWorld.length > 0
|
|
32
|
+
? operationData.strokePointsWorld
|
|
33
|
+
: [operationData.centerWorld];
|
|
34
|
+
const baseBounds = baseExtent.boundsIJK;
|
|
35
|
+
const baseCenterIJK = operationData.centerIJK;
|
|
36
|
+
const boundsForStroke = strokeCenters.reduce((acc, centerPoint) => {
|
|
37
|
+
if (!centerPoint) {
|
|
38
|
+
return acc;
|
|
39
|
+
}
|
|
40
|
+
const translatedCenterIJK = transformWorldToIndex(segmentationImageData, centerPoint);
|
|
41
|
+
const deltaIJK = [
|
|
42
|
+
translatedCenterIJK[0] - baseCenterIJK[0],
|
|
43
|
+
translatedCenterIJK[1] - baseCenterIJK[1],
|
|
44
|
+
translatedCenterIJK[2] - baseCenterIJK[2],
|
|
45
|
+
];
|
|
46
|
+
const translatedBounds = [
|
|
47
|
+
[baseBounds[0][0] + deltaIJK[0], baseBounds[0][1] + deltaIJK[0]],
|
|
48
|
+
[baseBounds[1][0] + deltaIJK[1], baseBounds[1][1] + deltaIJK[1]],
|
|
49
|
+
[baseBounds[2][0] + deltaIJK[2], baseBounds[2][1] + deltaIJK[2]],
|
|
50
|
+
];
|
|
51
|
+
if (!acc) {
|
|
52
|
+
return translatedBounds;
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
[
|
|
56
|
+
Math.min(acc[0][0], translatedBounds[0][0]),
|
|
57
|
+
Math.max(acc[0][1], translatedBounds[0][1]),
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
Math.min(acc[1][0], translatedBounds[1][0]),
|
|
61
|
+
Math.max(acc[1][1], translatedBounds[1][1]),
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
Math.min(acc[2][0], translatedBounds[2][0]),
|
|
65
|
+
Math.max(acc[2][1], translatedBounds[2][1]),
|
|
66
|
+
],
|
|
67
|
+
];
|
|
68
|
+
}, null);
|
|
69
|
+
const boundsToUse = boundsForStroke ?? baseExtent.boundsIJK;
|
|
70
|
+
if (segmentationImageData) {
|
|
71
|
+
const dimensions = segmentationImageData.getDimensions();
|
|
72
|
+
operationData.isInObjectBoundsIJK = [
|
|
73
|
+
[
|
|
74
|
+
Math.max(0, Math.min(boundsToUse[0][0], dimensions[0] - 1)),
|
|
75
|
+
Math.max(0, Math.min(boundsToUse[0][1], dimensions[0] - 1)),
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
Math.max(0, Math.min(boundsToUse[1][0], dimensions[1] - 1)),
|
|
79
|
+
Math.max(0, Math.min(boundsToUse[1][1], dimensions[1] - 1)),
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
Math.max(0, Math.min(boundsToUse[2][0], dimensions[2] - 1)),
|
|
83
|
+
Math.max(0, Math.min(boundsToUse[2][1], dimensions[2] - 1)),
|
|
84
|
+
],
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
operationData.isInObjectBoundsIJK = boundsToUse;
|
|
89
|
+
}
|
|
90
|
+
operationData.isInObject = createEllipseInPoint(cornersInWorld, {
|
|
91
|
+
strokePointsWorld: operationData.strokePointsWorld,
|
|
92
|
+
segmentationImageData,
|
|
93
|
+
radius: strokeRadius,
|
|
94
|
+
});
|
|
31
95
|
},
|
|
32
96
|
};
|
|
33
97
|
const SPHERE_STRATEGY = new BrushStrategy('Sphere', compositions.regionFill, compositions.setValue, sphereComposition, compositions.determineSegmentIndex, compositions.preview, compositions.labelmapStatistics, compositions.ensureSegmentationVolumeFor3DManipulation);
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "4.
|
|
1
|
+
export declare const version = "4.5.0";
|
package/dist/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '4.
|
|
1
|
+
export const version = '4.5.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cornerstonejs/tools",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Cornerstone3D Tools",
|
|
5
5
|
"types": "./dist/esm/index.d.ts",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"canvas": "3.1.0"
|
|
109
109
|
},
|
|
110
110
|
"peerDependencies": {
|
|
111
|
-
"@cornerstonejs/core": "4.
|
|
111
|
+
"@cornerstonejs/core": "4.5.0",
|
|
112
112
|
"@kitware/vtk.js": "32.12.1",
|
|
113
113
|
"@types/d3-array": "3.2.1",
|
|
114
114
|
"@types/d3-interpolate": "3.0.4",
|
|
@@ -127,5 +127,5 @@
|
|
|
127
127
|
"type": "individual",
|
|
128
128
|
"url": "https://ohif.org/donate"
|
|
129
129
|
},
|
|
130
|
-
"gitHead": "
|
|
130
|
+
"gitHead": "b678e3f34fb4984c6ba94c1564e60b63ae08cd42"
|
|
131
131
|
}
|