@cornerstonejs/tools 4.12.3 → 4.12.4
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/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/PlanarFreehandROITool.js +4 -4
- package/dist/esm/tools/segmentation/BrushTool.js +9 -0
- package/dist/esm/types/ISculptToolShape.d.ts +6 -4
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
import type { ISculptToolShape } from '../../types/ISculptToolShape';
|
|
3
|
-
import type { SculptData } from '../SculptorTool';
|
|
4
3
|
import type { SVGDrawingHelper, EventTypes, ContourAnnotationData } from '../../types';
|
|
5
4
|
export type PushedHandles = {
|
|
6
5
|
first?: number;
|
|
@@ -8,19 +7,18 @@ export type PushedHandles = {
|
|
|
8
7
|
};
|
|
9
8
|
declare class CircleSculptCursor implements ISculptToolShape {
|
|
10
9
|
static shapeName: string;
|
|
11
|
-
static readonly CHAIN_MAINTENANCE_ITERATIONS = 3;
|
|
12
|
-
static readonly CHAIN_PULL_STRENGTH_FACTOR = 0.3;
|
|
13
|
-
static readonly MAX_INTER_DISTANCE_FACTOR = 1.2;
|
|
14
10
|
private toolInfo;
|
|
15
11
|
renderShape(svgDrawingHelper: SVGDrawingHelper, canvasLocation: Types.Point2, options: unknown): void;
|
|
16
|
-
pushHandles(viewport: Types.IViewport, sculptData: SculptData): PushedHandles;
|
|
17
12
|
configureToolSize(evt: EventTypes.InteractionEventType): void;
|
|
18
13
|
updateToolSize(canvasCoords: Types.Point2, viewport: Types.IViewport, activeAnnotation: ContourAnnotationData): void;
|
|
19
14
|
getMaxSpacing(minSpacing: number): number;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
computeWorldRadius(viewport: any, clearExisting?: boolean): any;
|
|
16
|
+
getEdge(viewport: any, p1: Types.Point3, p2: Types.Point3, mouseCanvas: Types.Point2): {
|
|
17
|
+
point: any;
|
|
18
|
+
angle: number;
|
|
19
|
+
canvasPoint: any;
|
|
20
|
+
};
|
|
21
|
+
interpolatePoint(viewport: any, angle: any, center: any): any;
|
|
22
|
+
isInCursor(point: any, mousePoint: any): boolean;
|
|
25
23
|
}
|
|
26
24
|
export default CircleSculptCursor;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { vec3 } from 'gl-matrix';
|
|
1
|
+
import { vec2, vec3 } from 'gl-matrix';
|
|
2
2
|
import { getEnabledElement } from '@cornerstonejs/core';
|
|
3
3
|
import { distancePointToContour } from '../distancePointToContour';
|
|
4
4
|
import { drawCircle as drawCircleSvg } from '../../drawingSvg';
|
|
@@ -7,46 +7,15 @@ class CircleSculptCursor {
|
|
|
7
7
|
constructor() {
|
|
8
8
|
this.toolInfo = {
|
|
9
9
|
toolSize: null,
|
|
10
|
+
radius: null,
|
|
10
11
|
maxToolSize: null,
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
static { this.shapeName = 'Circle'; }
|
|
14
|
-
static { this.CHAIN_MAINTENANCE_ITERATIONS = 3; }
|
|
15
|
-
static { this.CHAIN_PULL_STRENGTH_FACTOR = 0.3; }
|
|
16
|
-
static { this.MAX_INTER_DISTANCE_FACTOR = 1.2; }
|
|
17
15
|
renderShape(svgDrawingHelper, canvasLocation, options) {
|
|
18
16
|
const circleUID = '0';
|
|
19
17
|
drawCircleSvg(svgDrawingHelper, 'SculptorTool', circleUID, canvasLocation, this.toolInfo.toolSize, options);
|
|
20
18
|
}
|
|
21
|
-
pushHandles(viewport, sculptData) {
|
|
22
|
-
const { points, mouseCanvasPoint } = sculptData;
|
|
23
|
-
const pushedHandles = { first: undefined, last: undefined };
|
|
24
|
-
const worldRadius = point.distanceToPoint(viewport.canvasToWorld(mouseCanvasPoint), viewport.canvasToWorld([
|
|
25
|
-
mouseCanvasPoint[0] + this.toolInfo.toolSize,
|
|
26
|
-
mouseCanvasPoint[1],
|
|
27
|
-
]));
|
|
28
|
-
for (let i = 0; i < points.length; i++) {
|
|
29
|
-
const handleCanvasPoint = viewport.worldToCanvas(points[i]);
|
|
30
|
-
const distanceToHandle = point.distanceToPoint(handleCanvasPoint, mouseCanvasPoint);
|
|
31
|
-
if (distanceToHandle > this.toolInfo.toolSize) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
this.pushOneHandle(i, worldRadius, sculptData);
|
|
35
|
-
if (pushedHandles.first === undefined) {
|
|
36
|
-
pushedHandles.first = i;
|
|
37
|
-
pushedHandles.last = i;
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
pushedHandles.last = i;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (pushedHandles.first !== undefined && pushedHandles.last !== undefined) {
|
|
44
|
-
for (let i = 0; i < CircleSculptCursor.CHAIN_MAINTENANCE_ITERATIONS; i++) {
|
|
45
|
-
this.maintainChainStructure(sculptData, pushedHandles);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return pushedHandles;
|
|
49
|
-
}
|
|
50
19
|
configureToolSize(evt) {
|
|
51
20
|
const toolInfo = this.toolInfo;
|
|
52
21
|
if (toolInfo.toolSize && toolInfo.maxToolSize) {
|
|
@@ -55,8 +24,9 @@ class CircleSculptCursor {
|
|
|
55
24
|
const eventData = evt.detail;
|
|
56
25
|
const element = eventData.element;
|
|
57
26
|
const minDim = Math.min(element.clientWidth, element.clientHeight);
|
|
58
|
-
const maxRadius = minDim /
|
|
27
|
+
const maxRadius = minDim / 24;
|
|
59
28
|
toolInfo.toolSize = maxRadius;
|
|
29
|
+
toolInfo.radius = null;
|
|
60
30
|
toolInfo.maxToolSize = maxRadius;
|
|
61
31
|
}
|
|
62
32
|
updateToolSize(canvasCoords, viewport, activeAnnotation) {
|
|
@@ -64,114 +34,44 @@ class CircleSculptCursor {
|
|
|
64
34
|
const radius = distancePointToContour(viewport, activeAnnotation, canvasCoords);
|
|
65
35
|
if (radius > 0) {
|
|
66
36
|
toolInfo.toolSize = Math.min(toolInfo.maxToolSize, radius);
|
|
37
|
+
this.computeWorldRadius(viewport, true);
|
|
67
38
|
}
|
|
68
39
|
}
|
|
69
40
|
getMaxSpacing(minSpacing) {
|
|
70
41
|
return Math.max(this.toolInfo.toolSize / 4, minSpacing);
|
|
71
42
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const { viewport } = enabledElement;
|
|
78
|
-
const previousCanvasPoint = viewport.worldToCanvas(points[previousIndex]);
|
|
79
|
-
const nextCanvasPoint = viewport.worldToCanvas(points[nextIndex]);
|
|
80
|
-
const midPoint = [
|
|
81
|
-
(previousCanvasPoint[0] + nextCanvasPoint[0]) / 2.0,
|
|
82
|
-
(previousCanvasPoint[1] + nextCanvasPoint[1]) / 2.0,
|
|
83
|
-
];
|
|
84
|
-
const distanceToMidPoint = point.distanceToPoint(mouseCanvasPoint, midPoint);
|
|
85
|
-
if (distanceToMidPoint < toolSize) {
|
|
86
|
-
const directionUnitVector = {
|
|
87
|
-
x: (midPoint[0] - mouseCanvasPoint[0]) / distanceToMidPoint,
|
|
88
|
-
y: (midPoint[1] - mouseCanvasPoint[1]) / distanceToMidPoint,
|
|
89
|
-
};
|
|
90
|
-
insertPosition = [
|
|
91
|
-
mouseCanvasPoint[0] + toolSize * directionUnitVector.x,
|
|
92
|
-
mouseCanvasPoint[1] + toolSize * directionUnitVector.y,
|
|
93
|
-
];
|
|
43
|
+
computeWorldRadius(viewport, clearExisting = false) {
|
|
44
|
+
if (!this.toolInfo.radius || clearExisting) {
|
|
45
|
+
const p0 = viewport.canvasToWorld([0, 0]);
|
|
46
|
+
const p1 = viewport.canvasToWorld([this.toolInfo.toolSize, 0]);
|
|
47
|
+
this.toolInfo.radius = vec3.length(vec3.sub(vec3.create(), p0, p1));
|
|
94
48
|
}
|
|
95
|
-
|
|
96
|
-
insertPosition = midPoint;
|
|
97
|
-
}
|
|
98
|
-
const worldPosition = viewport.canvasToWorld(insertPosition);
|
|
99
|
-
return worldPosition;
|
|
49
|
+
return this.toolInfo.radius;
|
|
100
50
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
51
|
+
getEdge(viewport, p1, p2, mouseCanvas) {
|
|
52
|
+
const midPoint = vec3.add(vec3.create(), p1, p2 || p1);
|
|
53
|
+
vec3.scale(midPoint, midPoint, 0.5);
|
|
54
|
+
const canvasMidPoint = viewport.worldToCanvas(midPoint);
|
|
55
|
+
const canvasDelta = vec2.sub(vec2.create(), canvasMidPoint, mouseCanvas);
|
|
56
|
+
const angle = Math.atan2(canvasDelta[1], canvasDelta[0]);
|
|
57
|
+
const point = this.interpolatePoint(viewport, angle, mouseCanvas);
|
|
58
|
+
const canvasPoint = viewport.worldToCanvas(point);
|
|
59
|
+
return {
|
|
60
|
+
point,
|
|
61
|
+
angle,
|
|
62
|
+
canvasPoint,
|
|
63
|
+
};
|
|
109
64
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]
|
|
65
|
+
interpolatePoint(viewport, angle, center) {
|
|
66
|
+
const [cx, cy] = center;
|
|
67
|
+
const r = this.toolInfo.toolSize;
|
|
68
|
+
const dx = Math.cos(angle) * r;
|
|
69
|
+
const dy = Math.sin(angle) * r;
|
|
70
|
+
const newPoint2 = [cx + dx, cy + dy];
|
|
71
|
+
return viewport.canvasToWorld(newPoint2);
|
|
116
72
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
let totalDistance = 0;
|
|
122
|
-
const numPoints = points.length;
|
|
123
|
-
for (let i = 0; i < numPoints; i++) {
|
|
124
|
-
const nextIndex = (i + 1) % numPoints;
|
|
125
|
-
const distance = point.distanceToPoint(points[i], points[nextIndex]);
|
|
126
|
-
totalDistance += distance;
|
|
127
|
-
}
|
|
128
|
-
return totalDistance / numPoints;
|
|
129
|
-
}
|
|
130
|
-
maintainChainStructure(sculptData, pushedHandles) {
|
|
131
|
-
const { points } = sculptData;
|
|
132
|
-
const first = pushedHandles.first;
|
|
133
|
-
const last = pushedHandles.last;
|
|
134
|
-
const mean = Math.round((first + last) / 2);
|
|
135
|
-
const numPoints = points.length;
|
|
136
|
-
if (!sculptData.meanDistance) {
|
|
137
|
-
sculptData.meanDistance = this.calculateMeanConsecutiveDistance(points);
|
|
138
|
-
}
|
|
139
|
-
const maxInterDistance = sculptData.meanDistance * CircleSculptCursor.MAX_INTER_DISTANCE_FACTOR;
|
|
140
|
-
for (let i = mean; i >= 0; i--) {
|
|
141
|
-
if (i >= numPoints - 1 || i < 0) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const nextIndex = i + 1;
|
|
145
|
-
const distanceToNext = point.distanceToPoint(points[i], points[nextIndex]);
|
|
146
|
-
if (distanceToNext > maxInterDistance) {
|
|
147
|
-
const pullDirection = this.directionalVector(points[i], points[nextIndex]);
|
|
148
|
-
const pullStrength = (distanceToNext - sculptData.meanDistance) / sculptData.meanDistance;
|
|
149
|
-
const adjustmentMagnitude = pullStrength *
|
|
150
|
-
sculptData.meanDistance *
|
|
151
|
-
CircleSculptCursor.CHAIN_PULL_STRENGTH_FACTOR;
|
|
152
|
-
points[i][0] += pullDirection[0] * adjustmentMagnitude;
|
|
153
|
-
points[i][1] += pullDirection[1] * adjustmentMagnitude;
|
|
154
|
-
points[i][2] += pullDirection[2] * adjustmentMagnitude;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
for (let i = mean + 1; i < numPoints; i++) {
|
|
158
|
-
if (i >= numPoints || i <= 0) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const previousIndex = i - 1;
|
|
162
|
-
const distanceToPrevious = point.distanceToPoint(points[i], points[previousIndex]);
|
|
163
|
-
if (distanceToPrevious > maxInterDistance) {
|
|
164
|
-
const pullDirection = this.directionalVector(points[i], points[previousIndex]);
|
|
165
|
-
const pullStrength = (distanceToPrevious - sculptData.meanDistance) /
|
|
166
|
-
sculptData.meanDistance;
|
|
167
|
-
const adjustmentMagnitude = pullStrength *
|
|
168
|
-
sculptData.meanDistance *
|
|
169
|
-
CircleSculptCursor.CHAIN_PULL_STRENGTH_FACTOR;
|
|
170
|
-
points[i][0] += pullDirection[0] * adjustmentMagnitude;
|
|
171
|
-
points[i][1] += pullDirection[1] * adjustmentMagnitude;
|
|
172
|
-
points[i][2] += pullDirection[2] * adjustmentMagnitude;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
73
|
+
isInCursor(point, mousePoint) {
|
|
74
|
+
return vec3.distance(point, mousePoint) < this.toolInfo.radius;
|
|
175
75
|
}
|
|
176
76
|
}
|
|
177
77
|
export default CircleSculptCursor;
|
|
@@ -2,15 +2,27 @@ import type { Types } from '@cornerstonejs/core';
|
|
|
2
2
|
import { BaseTool } from './base';
|
|
3
3
|
import type { EventTypes, PublicToolProps, ToolProps, SVGDrawingHelper } from '../types';
|
|
4
4
|
import type { ISculptToolShape } from '../types/ISculptToolShape';
|
|
5
|
+
export type Contour = {
|
|
6
|
+
annotationUID: string;
|
|
7
|
+
points: Array<Types.Point3>;
|
|
8
|
+
};
|
|
5
9
|
export type SculptData = {
|
|
6
10
|
mousePoint: Types.Point3;
|
|
7
|
-
deltaWorld: Types.Point3;
|
|
8
11
|
mouseCanvasPoint: Types.Point2;
|
|
9
12
|
points: Array<Types.Point3>;
|
|
10
13
|
maxSpacing: number;
|
|
11
|
-
meanDistance?: number;
|
|
12
14
|
element: HTMLDivElement;
|
|
15
|
+
contours: Contour[];
|
|
16
|
+
};
|
|
17
|
+
export type SculptIntersect = {
|
|
18
|
+
annotationUID: string;
|
|
19
|
+
isEnter: boolean;
|
|
20
|
+
index: number;
|
|
21
|
+
relIndex?: number;
|
|
22
|
+
point: Types.Point3;
|
|
23
|
+
angle: number;
|
|
13
24
|
};
|
|
25
|
+
export type ContourSelection = Array<SculptIntersect>;
|
|
14
26
|
declare class SculptorTool extends BaseTool {
|
|
15
27
|
static toolName: string;
|
|
16
28
|
registeredShapes: Map<any, any>;
|
|
@@ -23,13 +35,16 @@ declare class SculptorTool extends BaseTool {
|
|
|
23
35
|
preMouseDownCallback: (evt: EventTypes.InteractionEventType) => boolean;
|
|
24
36
|
mouseMoveCallback: (evt: EventTypes.InteractionEventType) => void;
|
|
25
37
|
protected sculpt(eventData: any, points: Array<Types.Point3>): void;
|
|
38
|
+
intersect(viewport: Types.IViewport, cursorShape: any): SculptIntersect[];
|
|
39
|
+
interpolatePoints(viewport: any, enter: any, exit: any, existing: any, newPoints: any): void;
|
|
40
|
+
getContourSelections(intersections: any, pointLength: any): ContourSelection[];
|
|
41
|
+
findMergeable(contours: any, testIntersection: any, currentIndex: any): any;
|
|
42
|
+
findNext(intersections: any, lastAngle: any, isEnter?: boolean): any;
|
|
26
43
|
protected interpolatePointsWithinMaxSpacing(i: number, points: Array<Types.Point3>, indicesToInsertAfter: Array<number>, maxSpacing: number): void;
|
|
27
44
|
private updateCursor;
|
|
45
|
+
protected getToolInstance(element: HTMLDivElement): any;
|
|
28
46
|
private filterSculptableAnnotationsForElement;
|
|
29
47
|
private configureToolSize;
|
|
30
|
-
private insertNewHandles;
|
|
31
|
-
private findNewHandleIndices;
|
|
32
|
-
private insertHandleRadially;
|
|
33
48
|
private selectFreehandTool;
|
|
34
49
|
private getClosestFreehandToolOnElement;
|
|
35
50
|
private endCallback;
|
|
@@ -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
|
};
|
|
@@ -255,12 +255,12 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
|
|
|
255
255
|
this._throttledCalculateCachedStats = throttle(this._calculateCachedStats, 100, { trailing: true });
|
|
256
256
|
}
|
|
257
257
|
filterInteractableAnnotationsForElement(element, annotations) {
|
|
258
|
-
if (!annotations
|
|
259
|
-
return;
|
|
258
|
+
if (!annotations?.length) {
|
|
259
|
+
return [];
|
|
260
260
|
}
|
|
261
261
|
const baseFilteredAnnotations = super.filterInteractableAnnotationsForElement(element, annotations);
|
|
262
|
-
if (!baseFilteredAnnotations
|
|
263
|
-
return;
|
|
262
|
+
if (!baseFilteredAnnotations?.length) {
|
|
263
|
+
return [];
|
|
264
264
|
}
|
|
265
265
|
const enabledElement = getEnabledElement(element);
|
|
266
266
|
const { viewport } = enabledElement;
|
|
@@ -113,6 +113,9 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
113
113
|
const hoverData = this._hoverData || this.createHoverData(element);
|
|
114
114
|
triggerAnnotationRenderForViewportUIDs(hoverData.viewportIdsToRender);
|
|
115
115
|
const operationData = this.getOperationData(element);
|
|
116
|
+
if (!operationData) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
116
119
|
this.applyActiveStrategyCallback(enabledElement, operationData, StrategyCallbacks.OnInteractionStart);
|
|
117
120
|
return true;
|
|
118
121
|
};
|
|
@@ -218,6 +221,9 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
218
221
|
this._hoverData = this.createHoverData(element, currentCanvas);
|
|
219
222
|
this._calculateCursor(element, currentCanvas);
|
|
220
223
|
const operationData = this.getOperationData(element);
|
|
224
|
+
if (!operationData) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
221
227
|
operationData.strokePointsWorld = [
|
|
222
228
|
vec3.clone(this._lastDragInfo.world),
|
|
223
229
|
vec3.clone(currentWorld),
|
|
@@ -238,6 +244,9 @@ class BrushTool extends LabelmapBaseTool {
|
|
|
238
244
|
const { element } = eventData;
|
|
239
245
|
const enabledElement = getEnabledElement(element);
|
|
240
246
|
const operationData = this.getOperationData(element);
|
|
247
|
+
if (!operationData) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
241
250
|
if (!this._previewData.preview && !this._previewData.isDrag) {
|
|
242
251
|
this.applyActiveStrategy(enabledElement, operationData);
|
|
243
252
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
import type { SVGDrawingHelper, EventTypes, ContourAnnotation } from '.';
|
|
3
|
-
import type { PushedHandles } from '../tools/SculptorTool/CircleSculptCursor';
|
|
4
|
-
import type { SculptData } from '../tools/SculptorTool';
|
|
5
3
|
export interface ISculptToolShape {
|
|
6
4
|
renderShape(svgDrawingHelper: SVGDrawingHelper, canvasLocation: Types.Point2, options: any): void;
|
|
7
|
-
pushHandles(viewport: Types.IViewport, sculptData: SculptData): PushedHandles;
|
|
8
5
|
configureToolSize(evt: EventTypes.InteractionEventType): void;
|
|
6
|
+
interpolatePoint(viewport: Types.IViewport, angle: number, center: Types.Point2): Types.Point2;
|
|
7
|
+
getEdge(viewport: Types.IViewport, p1: Types.Point3, p2: Types.Point3, mouseCanvas: Types.Point2): {
|
|
8
|
+
point: Types.Point3;
|
|
9
|
+
angle: number;
|
|
10
|
+
canvasPoint: Types.Point2;
|
|
11
|
+
};
|
|
9
12
|
updateToolSize(canvasCoords: Types.Point2, viewport: Types.IViewport, activeAnnotation: ContourAnnotation): void;
|
|
10
13
|
getMaxSpacing(minSpacing: number): number;
|
|
11
|
-
getInsertPosition(previousIndex: number, nextIndex: number, sculptData: SculptData): Types.Point3;
|
|
12
14
|
}
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "4.12.
|
|
1
|
+
export declare const version = "4.12.4";
|
package/dist/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '4.12.
|
|
1
|
+
export const version = '4.12.4';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cornerstonejs/tools",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.4",
|
|
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.2.0"
|
|
109
109
|
},
|
|
110
110
|
"peerDependencies": {
|
|
111
|
-
"@cornerstonejs/core": "4.12.
|
|
111
|
+
"@cornerstonejs/core": "4.12.4",
|
|
112
112
|
"@kitware/vtk.js": "34.15.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": "709d5a61b1acccf6a2dd8f9193907f98b5ed6c4c"
|
|
131
131
|
}
|