@cornerstonejs/tools 4.4.2 → 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.
@@ -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
- this._previewData.preview = this.applyActiveStrategy(enabledElement, this.getOperationData(element));
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 = currentPoints.canvas;
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,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[]): (pointLPS: Types.Point3) => boolean;
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 circleCornersIJK = points.map((world) => {
37
- return transformWorldToIndex(segmentationImageData, world);
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.isInObject = createPointInEllipse(cornersInWorld);
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) => pointInSphere(sphereObj, 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, pointLPS, center);
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 { boundsIJK: newBoundsIJK } = getSphereBoundsInfoFromViewport(points.slice(0, 2), segmentationImageData, viewport);
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
- operationData.isInObjectBoundsIJK = newBoundsIJK;
30
- operationData.isInObject = createEllipseInPoint(cornersInWorld);
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);
@@ -12,6 +12,7 @@ type LabelmapToolOperationData = {
12
12
  viewUp: number[];
13
13
  activeStrategy: string;
14
14
  points: Types.Point3[];
15
+ strokePointsWorld?: Types.Point3[];
15
16
  voxelManager: any;
16
17
  override: {
17
18
  voxelManager: Types.IVoxelManager<number>;
@@ -1 +1 @@
1
- export declare const version = "4.4.2";
1
+ export declare const version = "4.5.0";
@@ -1 +1 @@
1
- export const version = '4.4.2';
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.4.2",
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.4.2",
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": "e9db49c820e93f4b404ff0aa918ccb013191bebe"
130
+ "gitHead": "b678e3f34fb4984c6ba94c1564e60b63ae08cd42"
131
131
  }