@cornerstonejs/tools 4.18.2 → 4.18.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.
Files changed (35) hide show
  1. package/dist/esm/tools/VolumeCroppingControlTool.d.ts +10 -35
  2. package/dist/esm/tools/VolumeCroppingControlTool.js +179 -699
  3. package/dist/esm/tools/VolumeCroppingTool.d.ts +32 -32
  4. package/dist/esm/tools/VolumeCroppingTool.js +775 -525
  5. package/dist/esm/utilities/draw3D/addLine3DBetweenPoints.d.ts +7 -0
  6. package/dist/esm/utilities/draw3D/addLine3DBetweenPoints.js +34 -0
  7. package/dist/esm/utilities/draw3D/calculateAdaptiveSphereRadius.d.ts +6 -0
  8. package/dist/esm/utilities/draw3D/calculateAdaptiveSphereRadius.js +7 -0
  9. package/dist/esm/utilities/draw3D/index.d.ts +2 -0
  10. package/dist/esm/utilities/draw3D/index.js +2 -0
  11. package/dist/esm/utilities/index.d.ts +2 -1
  12. package/dist/esm/utilities/index.js +2 -1
  13. package/dist/esm/utilities/volumeCropping/computePlanePlaneIntersection.d.ts +6 -0
  14. package/dist/esm/utilities/volumeCropping/computePlanePlaneIntersection.js +37 -0
  15. package/dist/esm/utilities/volumeCropping/constants.d.ts +31 -0
  16. package/dist/esm/utilities/volumeCropping/constants.js +31 -0
  17. package/dist/esm/utilities/volumeCropping/copyClippingPlanes.d.ts +2 -0
  18. package/dist/esm/utilities/volumeCropping/copyClippingPlanes.js +6 -0
  19. package/dist/esm/utilities/volumeCropping/extractVolumeDirectionVectors.d.ts +9 -0
  20. package/dist/esm/utilities/volumeCropping/extractVolumeDirectionVectors.js +9 -0
  21. package/dist/esm/utilities/volumeCropping/findLineBoundsIntersection.d.ts +5 -0
  22. package/dist/esm/utilities/volumeCropping/findLineBoundsIntersection.js +50 -0
  23. package/dist/esm/utilities/volumeCropping/getColorKeyForPlaneIndex.d.ts +1 -0
  24. package/dist/esm/utilities/volumeCropping/getColorKeyForPlaneIndex.js +13 -0
  25. package/dist/esm/utilities/volumeCropping/getOrientationFromNormal.d.ts +2 -0
  26. package/dist/esm/utilities/volumeCropping/getOrientationFromNormal.js +19 -0
  27. package/dist/esm/utilities/volumeCropping/index.d.ts +9 -0
  28. package/dist/esm/utilities/volumeCropping/index.js +9 -0
  29. package/dist/esm/utilities/volumeCropping/parseCornerKey.d.ts +8 -0
  30. package/dist/esm/utilities/volumeCropping/parseCornerKey.js +11 -0
  31. package/dist/esm/utilities/volumeCropping/types.d.ts +5 -0
  32. package/dist/esm/utilities/volumeCropping/types.js +0 -0
  33. package/dist/esm/version.d.ts +1 -1
  34. package/dist/esm/version.js +1 -1
  35. package/package.json +3 -3
@@ -1,6 +1,4 @@
1
1
  import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData';
2
- import vtkPoints from '@kitware/vtk.js/Common/Core/Points';
3
- import vtkCellArray from '@kitware/vtk.js/Common/Core/CellArray';
4
2
  import { mat3, mat4, vec3 } from 'gl-matrix';
5
3
  import vtkMath from '@kitware/vtk.js/Common/Core/Math';
6
4
  import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
@@ -11,30 +9,8 @@ import { BaseTool } from './base';
11
9
  import { getRenderingEngine, getEnabledElementByIds, getEnabledElement, Enums, triggerEvent, eventTarget, } from '@cornerstonejs/core';
12
10
  import { getToolGroup } from '../store/ToolGroupManager';
13
11
  import { Events } from '../enums';
14
- const PLANEINDEX = {
15
- XMIN: 0,
16
- XMAX: 1,
17
- YMIN: 2,
18
- YMAX: 3,
19
- ZMIN: 4,
20
- ZMAX: 5,
21
- };
22
- const SPHEREINDEX = {
23
- XMIN: 0,
24
- XMAX: 1,
25
- YMIN: 2,
26
- YMAX: 3,
27
- ZMIN: 4,
28
- ZMAX: 5,
29
- XMIN_YMIN_ZMIN: 6,
30
- XMIN_YMIN_ZMAX: 7,
31
- XMIN_YMAX_ZMIN: 8,
32
- XMIN_YMAX_ZMAX: 9,
33
- XMAX_YMIN_ZMIN: 10,
34
- XMAX_YMIN_ZMAX: 11,
35
- XMAX_YMAX_ZMIN: 12,
36
- XMAX_YMAX_ZMAX: 13,
37
- };
12
+ import { PLANEINDEX, SPHEREINDEX, NUM_CLIPPING_PLANES, extractVolumeDirectionVectors, parseCornerKey, copyClippingPlanes, } from '../utilities/volumeCropping';
13
+ import { addLine3DBetweenPoints, calculateAdaptiveSphereRadius, } from '../utilities/draw3D';
38
14
  class VolumeCroppingTool extends BaseTool {
39
15
  constructor(toolProps = {}, defaultToolProps = {
40
16
  configuration: {
@@ -56,6 +32,7 @@ class VolumeCroppingTool extends BaseTool {
56
32
  grabSpherePixelDistance: 20,
57
33
  rotateIncrementDegrees: 2,
58
34
  rotateSampleDistanceFactor: 2,
35
+ rotateClippingPlanesIncrementDegrees: 5,
59
36
  },
60
37
  }) {
61
38
  super(toolProps, defaultToolProps);
@@ -63,16 +40,15 @@ class VolumeCroppingTool extends BaseTool {
63
40
  this._hasResolutionChanged = false;
64
41
  this.originalClippingPlanes = [];
65
42
  this.draggingSphereIndex = null;
66
- this.toolCenter = [0, 0, 0];
43
+ this.rotatePlanesOnDrag = false;
67
44
  this.cornerDragOffset = null;
68
45
  this.faceDragOffset = null;
46
+ this.volumeDirectionVectors = null;
69
47
  this.sphereStates = [];
70
48
  this.edgeLines = {};
71
49
  this.onSetToolConfiguration = () => {
72
- console.debug('Setting tool settoolconfiguration : volumeCropping');
73
50
  };
74
51
  this.onSetToolEnabled = () => {
75
- console.debug('Setting tool enabled: volumeCropping');
76
52
  };
77
53
  this.onCameraModified = (evt) => {
78
54
  const { element } = evt.currentTarget
@@ -115,9 +91,16 @@ class VolumeCroppingTool extends BaseTool {
115
91
  this.faceDragOffset = null;
116
92
  }
117
93
  else {
118
- const axisIdx = { x: 0, y: 1, z: 2 }[sphereState.axis];
94
+ const directionVector = this._getDirectionVectorForAxis(sphereState.axis);
95
+ const delta = [
96
+ sphereState.point[0] - mouseWorld[0],
97
+ sphereState.point[1] - mouseWorld[1],
98
+ sphereState.point[2] - mouseWorld[2],
99
+ ];
119
100
  this.faceDragOffset =
120
- sphereState.point[axisIdx] - mouseWorld[axisIdx];
101
+ delta[0] * directionVector[0] +
102
+ delta[1] * directionVector[1] +
103
+ delta[2] * directionVector[2];
121
104
  this.cornerDragOffset = null;
122
105
  }
123
106
  return true;
@@ -172,118 +155,143 @@ class VolumeCroppingTool extends BaseTool {
172
155
  return false;
173
156
  }
174
157
  if (sphereState.isCorner) {
175
- const newCorner = this._calculateNewCornerPosition(world);
176
- this._updateSpherePosition(sphereState, newCorner);
177
- const axisFlags = this._parseCornerKey(sphereState.uid);
178
- this._updateRelatedCorners(sphereState, newCorner, axisFlags);
179
- this._updateFaceSpheresFromCorners();
158
+ const newCorner = this.cornerDragOffset
159
+ ? vec3.add([0, 0, 0], world, this.cornerDragOffset)
160
+ : world;
161
+ const oldCorner = sphereState.point;
162
+ const axisFlags = parseCornerKey(sphereState.uid);
163
+ const { xDir, yDir, zDir } = this._getDirectionVectors();
164
+ if (!xDir || !yDir || !zDir)
165
+ return false;
166
+ const delta = [
167
+ newCorner[0] - oldCorner[0],
168
+ newCorner[1] - oldCorner[1],
169
+ newCorner[2] - oldCorner[2],
170
+ ];
171
+ const deltaX = delta[0] * xDir[0] + delta[1] * xDir[1] + delta[2] * xDir[2];
172
+ const deltaY = delta[0] * yDir[0] + delta[1] * yDir[1] + delta[2] * yDir[2];
173
+ const deltaZ = delta[0] * zDir[0] + delta[1] * zDir[1] + delta[2] * zDir[2];
174
+ if (axisFlags.isXMin) {
175
+ const faceXMin = this.sphereStates[SPHEREINDEX.XMIN];
176
+ const newPoint = [
177
+ faceXMin.point[0] + deltaX * xDir[0],
178
+ faceXMin.point[1] + deltaX * xDir[1],
179
+ faceXMin.point[2] + deltaX * xDir[2],
180
+ ];
181
+ this._updateSpherePosition(SPHEREINDEX.XMIN, newPoint);
182
+ }
183
+ else if (axisFlags.isXMax) {
184
+ const faceXMax = this.sphereStates[SPHEREINDEX.XMAX];
185
+ const newPoint = [
186
+ faceXMax.point[0] + deltaX * xDir[0],
187
+ faceXMax.point[1] + deltaX * xDir[1],
188
+ faceXMax.point[2] + deltaX * xDir[2],
189
+ ];
190
+ this._updateSpherePosition(SPHEREINDEX.XMAX, newPoint);
191
+ }
192
+ if (axisFlags.isYMin) {
193
+ const faceYMin = this.sphereStates[SPHEREINDEX.YMIN];
194
+ const newPoint = [
195
+ faceYMin.point[0] + deltaY * yDir[0],
196
+ faceYMin.point[1] + deltaY * yDir[1],
197
+ faceYMin.point[2] + deltaY * yDir[2],
198
+ ];
199
+ this._updateSpherePosition(SPHEREINDEX.YMIN, newPoint);
200
+ }
201
+ else if (axisFlags.isYMax) {
202
+ const faceYMax = this.sphereStates[SPHEREINDEX.YMAX];
203
+ const newPoint = [
204
+ faceYMax.point[0] + deltaY * yDir[0],
205
+ faceYMax.point[1] + deltaY * yDir[1],
206
+ faceYMax.point[2] + deltaY * yDir[2],
207
+ ];
208
+ this._updateSpherePosition(SPHEREINDEX.YMAX, newPoint);
209
+ }
210
+ if (axisFlags.isZMin) {
211
+ const faceZMin = this.sphereStates[SPHEREINDEX.ZMIN];
212
+ const newPoint = [
213
+ faceZMin.point[0] + deltaZ * zDir[0],
214
+ faceZMin.point[1] + deltaZ * zDir[1],
215
+ faceZMin.point[2] + deltaZ * zDir[2],
216
+ ];
217
+ this._updateSpherePosition(SPHEREINDEX.ZMIN, newPoint);
218
+ }
219
+ else if (axisFlags.isZMax) {
220
+ const faceZMax = this.sphereStates[SPHEREINDEX.ZMAX];
221
+ const newPoint = [
222
+ faceZMax.point[0] + deltaZ * zDir[0],
223
+ faceZMax.point[1] + deltaZ * zDir[1],
224
+ faceZMax.point[2] + deltaZ * zDir[2],
225
+ ];
226
+ this._updateSpherePosition(SPHEREINDEX.ZMAX, newPoint);
227
+ }
180
228
  this._updateCornerSpheres();
229
+ this._updateFaceSpheresFromCorners();
181
230
  }
182
231
  else {
183
- const axisIdx = { x: 0, y: 1, z: 2 }[sphereState.axis];
184
- let newValue = world[axisIdx];
185
- if (this.faceDragOffset !== null) {
186
- newValue += this.faceDragOffset;
187
- }
188
- sphereState.point[axisIdx] = newValue;
189
- sphereState.sphereSource.setCenter(...sphereState.point);
190
- sphereState.sphereSource.modified();
232
+ const directionVector = this._getDirectionVectorForAxis(sphereState.axis);
233
+ const delta = [
234
+ world[0] - sphereState.point[0],
235
+ world[1] - sphereState.point[1],
236
+ world[2] - sphereState.point[2],
237
+ ];
238
+ const distanceAlongAxis = delta[0] * directionVector[0] +
239
+ delta[1] * directionVector[1] +
240
+ delta[2] * directionVector[2];
241
+ const adjustedDistance = this.faceDragOffset !== null
242
+ ? distanceAlongAxis + this.faceDragOffset
243
+ : distanceAlongAxis;
244
+ const newPoint = [
245
+ sphereState.point[0] + adjustedDistance * directionVector[0],
246
+ sphereState.point[1] + adjustedDistance * directionVector[1],
247
+ sphereState.point[2] + adjustedDistance * directionVector[2],
248
+ ];
249
+ this._updateSpherePosition(this.draggingSphereIndex, newPoint);
191
250
  this._updateCornerSpheresFromFaces();
192
251
  this._updateFaceSpheresFromCorners();
193
252
  this._updateCornerSpheres();
194
253
  }
195
254
  this._updateClippingPlanesFromFaceSpheres(viewport);
196
255
  viewport.render();
197
- this._triggerToolChangedEvent(sphereState);
256
+ this._notifyClippingPlanesChanged();
198
257
  return true;
199
258
  };
200
259
  this._onControlToolChange = (evt) => {
201
260
  const viewport = this._getViewport();
202
- if (!evt.detail.toolCenter) {
203
- triggerEvent(eventTarget, Events.VOLUMECROPPING_TOOL_CHANGED, {
204
- originalClippingPlanes: this.originalClippingPlanes,
205
- viewportId: viewport.id,
206
- renderingEngineId: viewport.renderingEngineId,
207
- seriesInstanceUID: this.seriesInstanceUID,
208
- });
261
+ if (evt.detail.seriesInstanceUID !== this.seriesInstanceUID) {
262
+ return;
209
263
  }
210
- else {
211
- if (evt.detail.seriesInstanceUID !== this.seriesInstanceUID) {
212
- return;
213
- }
214
- const isMin = evt.detail.handleType === 'min';
215
- const toolCenter = isMin
216
- ? evt.detail.toolCenterMin
217
- : evt.detail.toolCenterMax;
218
- const normals = isMin
219
- ? [
220
- [1, 0, 0],
221
- [0, 1, 0],
222
- [0, 0, 1],
223
- ]
224
- : [
225
- [-1, 0, 0],
226
- [0, -1, 0],
227
- [0, 0, -1],
228
- ];
229
- const planeIndices = isMin
230
- ? [PLANEINDEX.XMIN, PLANEINDEX.YMIN, PLANEINDEX.ZMIN]
231
- : [PLANEINDEX.XMAX, PLANEINDEX.YMAX, PLANEINDEX.ZMAX];
232
- const sphereIndices = isMin
233
- ? [SPHEREINDEX.XMIN, SPHEREINDEX.YMIN, SPHEREINDEX.ZMIN]
234
- : [SPHEREINDEX.XMAX, SPHEREINDEX.YMAX, SPHEREINDEX.ZMAX];
235
- const axes = ['x', 'y', 'z'];
236
- const orientationAxes = [
237
- Enums.OrientationAxis.SAGITTAL,
238
- Enums.OrientationAxis.CORONAL,
239
- Enums.OrientationAxis.AXIAL,
240
- ];
241
- for (let i = 0; i < 3; ++i) {
242
- const origin = [0, 0, 0];
243
- origin[i] = toolCenter[i];
244
- const plane = vtkPlane.newInstance({
245
- origin,
246
- normal: normals[i],
247
- });
248
- this.originalClippingPlanes[planeIndices[i]].origin = plane.getOrigin();
249
- this.sphereStates[sphereIndices[i]].point[i] = plane.getOrigin()[i];
250
- this.sphereStates[sphereIndices[i]].sphereSource.setCenter(...this.sphereStates[sphereIndices[i]].point);
251
- this.sphereStates[sphereIndices[i]].sphereSource.modified();
252
- const otherSphere = this.sphereStates.find((s, idx) => s.axis === axes[i] && idx !== sphereIndices[i]);
253
- const newCenter = (otherSphere.point[i] + plane.getOrigin()[i]) / 2;
254
- this.sphereStates.forEach((state) => {
255
- if (!state.isCorner &&
256
- state.axis !== axes[i] &&
257
- !evt.detail.viewportOrientation.includes(orientationAxes[i])) {
258
- state.point[i] = newCenter;
259
- state.sphereSource.setCenter(state.point);
260
- state.sphereActor.getProperty().setColor(state.color);
261
- state.sphereSource.modified();
262
- }
263
- });
264
- const volumeActor = viewport.getDefaultActor()?.actor;
265
- if (volumeActor) {
266
- const mapper = volumeActor.getMapper();
267
- const clippingPlanes = mapper.getClippingPlanes();
268
- if (clippingPlanes) {
269
- clippingPlanes[planeIndices[i]].setOrigin(plane.getOrigin());
270
- }
271
- }
272
- }
264
+ if (evt.detail.clippingPlanes &&
265
+ evt.detail.clippingPlanes.length >= NUM_CLIPPING_PLANES) {
266
+ this.originalClippingPlanes = copyClippingPlanes(evt.detail.clippingPlanes);
267
+ this._updateFaceSpheresFromClippingPlanes();
268
+ this._updateCornerSpheresFromFaces();
269
+ this._updateFaceSpheresFromCorners();
273
270
  this._updateCornerSpheres();
271
+ const mapper = this._getVolumeMapper(viewport);
272
+ if (mapper) {
273
+ this._applyClippingPlanesToMapper(mapper);
274
+ }
274
275
  viewport.render();
276
+ this._notifyClippingPlanesChanged(viewport);
277
+ }
278
+ else {
279
+ this._notifyClippingPlanesChanged(viewport);
275
280
  }
276
281
  };
277
282
  this._getViewportsInfo = () => {
278
- const viewports = getToolGroup(this.toolGroupId).viewportsInfo;
279
- return viewports;
283
+ const toolGroup = getToolGroup(this.toolGroupId);
284
+ return toolGroup?.viewportsInfo || [];
280
285
  };
281
286
  this._initialize3DViewports = (viewportsInfo) => {
282
- if (!viewportsInfo || !viewportsInfo.length || !viewportsInfo[0]) {
287
+ if (!viewportsInfo?.length || !viewportsInfo[0]) {
283
288
  console.warn('VolumeCroppingTool: No viewportsInfo available for initialization of volumecroppingtool.');
284
289
  return;
285
290
  }
286
291
  const viewport = this._getViewport();
292
+ if (!viewport) {
293
+ return;
294
+ }
287
295
  const volumeActors = viewport.getActors();
288
296
  if (!volumeActors || volumeActors.length === 0) {
289
297
  console.warn('VolumeCroppingTool: No volume actors found in the viewport.');
@@ -295,78 +303,85 @@ class VolumeCroppingTool extends BaseTool {
295
303
  return;
296
304
  }
297
305
  this.seriesInstanceUID = imageData.seriesInstanceUID || 'unknown';
298
- const worldBounds = imageData.getBounds();
306
+ this.volumeDirectionVectors = extractVolumeDirectionVectors(imageData);
307
+ const { xDir, yDir, zDir } = this.volumeDirectionVectors;
308
+ const dimensions = imageData.getDimensions();
299
309
  const cropFactor = this.configuration.initialCropFactor || 0.1;
300
- const xRange = worldBounds[1] - worldBounds[0];
301
- const yRange = worldBounds[3] - worldBounds[2];
302
- const zRange = worldBounds[5] - worldBounds[4];
303
- const xMin = worldBounds[0] + cropFactor * xRange;
304
- const xMax = worldBounds[1] - cropFactor * xRange;
305
- const yMin = worldBounds[2] + cropFactor * yRange;
306
- const yMax = worldBounds[3] - cropFactor * yRange;
307
- const zMin = worldBounds[4] + cropFactor * zRange;
308
- const zMax = worldBounds[5] - cropFactor * zRange;
309
- const planes = [];
310
- const planeXmin = vtkPlane.newInstance({
311
- origin: [xMin, 0, 0],
312
- normal: [1, 0, 0],
310
+ const xMin = cropFactor * dimensions[0];
311
+ const xMax = (1 - cropFactor) * dimensions[0];
312
+ const yMin = cropFactor * dimensions[1];
313
+ const yMax = (1 - cropFactor) * dimensions[1];
314
+ const zMin = cropFactor * dimensions[2];
315
+ const zMax = (1 - cropFactor) * dimensions[2];
316
+ const xCenter = (xMin + xMax) / 2;
317
+ const yCenter = (yMin + yMax) / 2;
318
+ const zCenter = (zMin + zMax) / 2;
319
+ const faceXMin = imageData.indexToWorld([xMin, yCenter, zCenter]);
320
+ const faceXMax = imageData.indexToWorld([xMax, yCenter, zCenter]);
321
+ const faceYMin = imageData.indexToWorld([xCenter, yMin, zCenter]);
322
+ const faceYMax = imageData.indexToWorld([xCenter, yMax, zCenter]);
323
+ const faceZMin = imageData.indexToWorld([xCenter, yCenter, zMin]);
324
+ const faceZMax = imageData.indexToWorld([xCenter, yCenter, zMax]);
325
+ const planeXMin = vtkPlane.newInstance({
326
+ origin: faceXMin,
327
+ normal: xDir,
313
328
  });
314
- const planeXmax = vtkPlane.newInstance({
315
- origin: [xMax, 0, 0],
316
- normal: [-1, 0, 0],
329
+ const planeXMax = vtkPlane.newInstance({
330
+ origin: faceXMax,
331
+ normal: [-xDir[0], -xDir[1], -xDir[2]],
317
332
  });
318
- const planeYmin = vtkPlane.newInstance({
319
- origin: [0, yMin, 0],
320
- normal: [0, 1, 0],
333
+ const planeYMin = vtkPlane.newInstance({
334
+ origin: faceYMin,
335
+ normal: yDir,
321
336
  });
322
- const planeYmax = vtkPlane.newInstance({
323
- origin: [0, yMax, 0],
324
- normal: [0, -1, 0],
337
+ const planeYMax = vtkPlane.newInstance({
338
+ origin: faceYMax,
339
+ normal: [-yDir[0], -yDir[1], -yDir[2]],
325
340
  });
326
- const planeZmin = vtkPlane.newInstance({
327
- origin: [0, 0, zMin],
328
- normal: [0, 0, 1],
341
+ const planeZMin = vtkPlane.newInstance({
342
+ origin: faceZMin,
343
+ normal: zDir,
329
344
  });
330
- const planeZmax = vtkPlane.newInstance({
331
- origin: [0, 0, zMax],
332
- normal: [0, 0, -1],
345
+ const planeZMax = vtkPlane.newInstance({
346
+ origin: faceZMax,
347
+ normal: [-zDir[0], -zDir[1], -zDir[2]],
333
348
  });
334
- const mapper = viewport
335
- .getDefaultActor()
336
- .actor.getMapper();
337
- planes.push(planeXmin);
338
- planes.push(planeXmax);
339
- planes.push(planeYmin);
340
- planes.push(planeYmax);
341
- planes.push(planeZmin);
342
- planes.push(planeZmax);
349
+ const planes = [
350
+ planeXMin,
351
+ planeXMax,
352
+ planeYMin,
353
+ planeYMax,
354
+ planeZMin,
355
+ planeZMax,
356
+ ];
343
357
  const originalPlanes = planes.map((plane) => ({
344
358
  origin: [...plane.getOrigin()],
345
359
  normal: [...plane.getNormal()],
346
360
  }));
347
361
  this.originalClippingPlanes = originalPlanes;
348
- const sphereXminPoint = [xMin, (yMax + yMin) / 2, (zMax + zMin) / 2];
349
- const sphereXmaxPoint = [xMax, (yMax + yMin) / 2, (zMax + zMin) / 2];
350
- const sphereYminPoint = [(xMax + xMin) / 2, yMin, (zMax + zMin) / 2];
351
- const sphereYmaxPoint = [(xMax + xMin) / 2, yMax, (zMax + zMin) / 2];
352
- const sphereZminPoint = [(xMax + xMin) / 2, (yMax + yMin) / 2, zMin];
353
- const sphereZmaxPoint = [(xMax + xMin) / 2, (yMax + yMin) / 2, zMax];
354
- const adaptiveRadius = this._calculateAdaptiveSphereRadius(Math.sqrt(xRange * xRange + yRange * yRange + zRange * zRange));
355
- this._addSphere(viewport, sphereXminPoint, 'x', 'min', null, adaptiveRadius);
356
- this._addSphere(viewport, sphereXmaxPoint, 'x', 'max', null, adaptiveRadius);
357
- this._addSphere(viewport, sphereYminPoint, 'y', 'min', null, adaptiveRadius);
358
- this._addSphere(viewport, sphereYmaxPoint, 'y', 'max', null, adaptiveRadius);
359
- this._addSphere(viewport, sphereZminPoint, 'z', 'min', null, adaptiveRadius);
360
- this._addSphere(viewport, sphereZmaxPoint, 'z', 'max', null, adaptiveRadius);
362
+ const diag0 = imageData.indexToWorld([0, 0, 0]);
363
+ const diag1 = imageData.indexToWorld([
364
+ dimensions[0],
365
+ dimensions[1],
366
+ dimensions[2],
367
+ ]);
368
+ const diagonal = vec3.distance(diag0, diag1);
369
+ const adaptiveRadius = calculateAdaptiveSphereRadius(diagonal, this.configuration);
370
+ this._addSphere(viewport, faceXMin, 'x', 'min', null, adaptiveRadius);
371
+ this._addSphere(viewport, faceXMax, 'x', 'max', null, adaptiveRadius);
372
+ this._addSphere(viewport, faceYMin, 'y', 'min', null, adaptiveRadius);
373
+ this._addSphere(viewport, faceYMax, 'y', 'max', null, adaptiveRadius);
374
+ this._addSphere(viewport, faceZMin, 'z', 'min', null, adaptiveRadius);
375
+ this._addSphere(viewport, faceZMax, 'z', 'max', null, adaptiveRadius);
361
376
  const corners = [
362
- [xMin, yMin, zMin],
363
- [xMin, yMin, zMax],
364
- [xMin, yMax, zMin],
365
- [xMin, yMax, zMax],
366
- [xMax, yMin, zMin],
367
- [xMax, yMin, zMax],
368
- [xMax, yMax, zMin],
369
- [xMax, yMax, zMax],
377
+ imageData.indexToWorld([xMin, yMin, zMin]),
378
+ imageData.indexToWorld([xMin, yMin, zMax]),
379
+ imageData.indexToWorld([xMin, yMax, zMin]),
380
+ imageData.indexToWorld([xMin, yMax, zMax]),
381
+ imageData.indexToWorld([xMax, yMin, zMin]),
382
+ imageData.indexToWorld([xMax, yMin, zMax]),
383
+ imageData.indexToWorld([xMax, yMax, zMin]),
384
+ imageData.indexToWorld([xMax, yMax, zMax]),
370
385
  ];
371
386
  const cornerKeys = [
372
387
  'XMIN_YMIN_ZMIN',
@@ -395,21 +410,24 @@ class VolumeCroppingTool extends BaseTool {
395
410
  ['XMAX_YMIN_ZMIN', 'XMAX_YMIN_ZMAX'],
396
411
  ['XMAX_YMAX_ZMIN', 'XMAX_YMAX_ZMAX'],
397
412
  ];
398
- edgeCornerPairs.forEach(([key1, key2], i) => {
413
+ edgeCornerPairs.forEach(([key1, key2]) => {
399
414
  const state1 = this.sphereStates.find((s) => s.uid === `corner_${key1}`);
400
415
  const state2 = this.sphereStates.find((s) => s.uid === `corner_${key2}`);
401
416
  if (state1 && state2) {
402
417
  const uid = `edge_${key1}_${key2}`;
403
- const { actor, source } = this._addLine3DBetweenPoints(viewport, state1.point, state2.point, [0.7, 0.7, 0.7], uid);
418
+ const { actor, source } = addLine3DBetweenPoints(viewport, state1.point, state2.point, [0.7, 0.7, 0.7], uid, this.configuration.showHandles);
404
419
  this.edgeLines[uid] = { actor, source, key1, key2 };
405
420
  }
406
421
  });
407
- mapper.addClippingPlane(planeXmin);
408
- mapper.addClippingPlane(planeXmax);
409
- mapper.addClippingPlane(planeYmin);
410
- mapper.addClippingPlane(planeYmax);
411
- mapper.addClippingPlane(planeZmin);
412
- mapper.addClippingPlane(planeZmax);
422
+ const mapper = viewport
423
+ .getDefaultActor()
424
+ .actor.getMapper();
425
+ mapper.addClippingPlane(planeXMin);
426
+ mapper.addClippingPlane(planeXMax);
427
+ mapper.addClippingPlane(planeYMin);
428
+ mapper.addClippingPlane(planeYMax);
429
+ mapper.addClippingPlane(planeZMin);
430
+ mapper.addClippingPlane(planeZMax);
413
431
  eventTarget.addEventListener(Events.VOLUMECROPPINGCONTROL_TOOL_CHANGED, (evt) => {
414
432
  this._onControlToolChange(evt);
415
433
  });
@@ -423,107 +441,14 @@ class VolumeCroppingTool extends BaseTool {
423
441
  return { viewport, world };
424
442
  };
425
443
  this._getViewport = () => {
426
- const [viewport3D] = this._getViewportsInfo();
427
- const renderingEngine = getRenderingEngine(viewport3D.renderingEngineId);
428
- return renderingEngine.getViewport(viewport3D.viewportId);
429
- };
430
- this._handleCornerSphereMovement = (sphereState, world, viewport) => {
431
- const newCorner = this._calculateNewCornerPosition(world);
432
- this._updateSpherePosition(sphereState, newCorner);
433
- const axisFlags = this._parseCornerKey(sphereState.uid);
434
- this._updateRelatedCorners(sphereState, newCorner, axisFlags);
435
- this._updateAfterCornerMovement(viewport);
436
- };
437
- this._handleFaceSphereMovement = (sphereState, world, viewport) => {
438
- const axisIdx = { x: 0, y: 1, z: 2 }[sphereState.axis];
439
- let newValue = world[axisIdx];
440
- if (this.faceDragOffset !== null) {
441
- newValue += this.faceDragOffset;
442
- }
443
- sphereState.point[axisIdx] = newValue;
444
- sphereState.sphereSource.setCenter(...sphereState.point);
445
- sphereState.sphereSource.modified();
446
- this._updateAfterFaceMovement(viewport);
447
- };
448
- this._calculateNewCornerPosition = (world) => {
449
- let newCorner = [world[0], world[1], world[2]];
450
- if (this.cornerDragOffset) {
451
- newCorner = [
452
- world[0] + this.cornerDragOffset[0],
453
- world[1] + this.cornerDragOffset[1],
454
- world[2] + this.cornerDragOffset[2],
455
- ];
456
- }
457
- return newCorner;
458
- };
459
- this._parseCornerKey = (uid) => {
460
- const cornerKey = uid.replace('corner_', '');
461
- return {
462
- isXMin: cornerKey.includes('XMIN'),
463
- isXMax: cornerKey.includes('XMAX'),
464
- isYMin: cornerKey.includes('YMIN'),
465
- isYMax: cornerKey.includes('YMAX'),
466
- isZMin: cornerKey.includes('ZMIN'),
467
- isZMax: cornerKey.includes('ZMAX'),
468
- };
469
- };
470
- this._updateSpherePosition = (sphereState, newPosition) => {
471
- sphereState.point = newPosition;
472
- sphereState.sphereSource.setCenter(...newPosition);
473
- sphereState.sphereSource.modified();
474
- };
475
- this._updateRelatedCorners = (draggedSphere, newCorner, axisFlags) => {
476
- this.sphereStates.forEach((state) => {
477
- if (!state.isCorner || state === draggedSphere) {
478
- return;
479
- }
480
- const key = state.uid.replace('corner_', '');
481
- const shouldUpdate = this._shouldUpdateCorner(key, axisFlags);
482
- if (shouldUpdate) {
483
- this._updateCornerCoordinates(state, newCorner, key, axisFlags);
484
- }
485
- });
486
- };
487
- this._shouldUpdateCorner = (cornerKey, axisFlags) => {
488
- return ((axisFlags.isXMin && cornerKey.includes('XMIN')) ||
489
- (axisFlags.isXMax && cornerKey.includes('XMAX')) ||
490
- (axisFlags.isYMin && cornerKey.includes('YMIN')) ||
491
- (axisFlags.isYMax && cornerKey.includes('YMAX')) ||
492
- (axisFlags.isZMin && cornerKey.includes('ZMIN')) ||
493
- (axisFlags.isZMax && cornerKey.includes('ZMAX')));
494
- };
495
- this._updateCornerCoordinates = (state, newCorner, cornerKey, axisFlags) => {
496
- if ((axisFlags.isXMin && cornerKey.includes('XMIN')) ||
497
- (axisFlags.isXMax && cornerKey.includes('XMAX'))) {
498
- state.point[0] = newCorner[0];
499
- }
500
- if ((axisFlags.isYMin && cornerKey.includes('YMIN')) ||
501
- (axisFlags.isYMax && cornerKey.includes('YMAX'))) {
502
- state.point[1] = newCorner[1];
503
- }
504
- if ((axisFlags.isZMin && cornerKey.includes('ZMIN')) ||
505
- (axisFlags.isZMax && cornerKey.includes('ZMAX'))) {
506
- state.point[2] = newCorner[2];
444
+ const viewportsInfo = this._getViewportsInfo();
445
+ if (!viewportsInfo || viewportsInfo.length === 0) {
446
+ return null;
507
447
  }
508
- state.sphereSource.setCenter(...state.point);
509
- state.sphereSource.modified();
510
- };
511
- this._updateAfterCornerMovement = (viewport) => {
512
- this._updateFaceSpheresFromCorners();
513
- this._updateCornerSpheres();
514
- this._updateClippingPlanesFromFaceSpheres(viewport);
515
- };
516
- this._updateAfterFaceMovement = (viewport) => {
517
- this._updateCornerSpheresFromFaces();
518
- this._updateClippingPlanesFromFaceSpheres(viewport);
519
- };
520
- this._triggerToolChangedEvent = (sphereState) => {
521
- triggerEvent(eventTarget, Events.VOLUMECROPPING_TOOL_CHANGED, {
522
- toolCenter: sphereState.point,
523
- axis: sphereState.isCorner ? 'corner' : sphereState.axis,
524
- draggingSphereIndex: this.draggingSphereIndex,
525
- seriesInstanceUID: this.seriesInstanceUID,
526
- });
448
+ const [viewport3D] = viewportsInfo;
449
+ const renderingEngine = getRenderingEngine(viewport3D.renderingEngineId);
450
+ const viewport = renderingEngine?.getViewport(viewport3D.viewportId);
451
+ return viewport || null;
527
452
  };
528
453
  this._onNewVolume = () => {
529
454
  const viewportsInfo = this._getViewportsInfo();
@@ -532,6 +457,207 @@ class VolumeCroppingTool extends BaseTool {
532
457
  this.edgeLines = {};
533
458
  this._initialize3DViewports(viewportsInfo);
534
459
  };
460
+ this._rotateClippingPlanes = (evt) => {
461
+ const { element, currentPoints, lastPoints } = evt.detail;
462
+ const currentPointsCanvas = currentPoints.canvas;
463
+ const lastPointsCanvas = lastPoints.canvas;
464
+ const rotateIncrementDegrees = this.configuration.rotateClippingPlanesIncrementDegrees ??
465
+ this.configuration.rotateIncrementDegrees ??
466
+ 5;
467
+ const enabledElement = getEnabledElement(element);
468
+ const { viewport } = enabledElement;
469
+ const camera = viewport.getCamera();
470
+ const width = element.clientWidth;
471
+ const height = element.clientHeight;
472
+ const normalizedPosition = [
473
+ currentPointsCanvas[0] / width,
474
+ currentPointsCanvas[1] / height,
475
+ ];
476
+ const normalizedPreviousPosition = [
477
+ lastPointsCanvas[0] / width,
478
+ lastPointsCanvas[1] / height,
479
+ ];
480
+ const normalizedCenter = [0.5, 0.5];
481
+ const radsq = (1.0 + Math.abs(normalizedCenter[0])) ** 2.0;
482
+ const op = [normalizedPreviousPosition[0], 0, 0];
483
+ const oe = [normalizedPosition[0], 0, 0];
484
+ const opsq = op[0] ** 2;
485
+ const oesq = oe[0] ** 2;
486
+ const lop = opsq > radsq ? 0 : Math.sqrt(radsq - opsq);
487
+ const loe = oesq > radsq ? 0 : Math.sqrt(radsq - oesq);
488
+ const nop = [op[0], 0, lop];
489
+ vtkMath.normalize(nop);
490
+ const noe = [oe[0], 0, loe];
491
+ vtkMath.normalize(noe);
492
+ const dot = vtkMath.dot(nop, noe);
493
+ if (Math.abs(dot) > 0.0001) {
494
+ const angleX = 20 *
495
+ Math.acos(vtkMath.clampValue(dot, -1.0, 1.0)) *
496
+ Math.sign(normalizedPosition[0] - normalizedPreviousPosition[0]) *
497
+ rotateIncrementDegrees;
498
+ const upVec = camera.viewUp;
499
+ const atV = camera.viewPlaneNormal;
500
+ const rightV = [0, 0, 0];
501
+ const forwardV = [0, 0, 0];
502
+ vtkMath.cross(upVec, atV, rightV);
503
+ vtkMath.normalize(rightV);
504
+ vtkMath.cross(atV, rightV, forwardV);
505
+ vtkMath.normalize(forwardV);
506
+ const angleY = 20 *
507
+ (normalizedPosition[1] - normalizedPreviousPosition[1]) *
508
+ rotateIncrementDegrees;
509
+ let rotationCenter;
510
+ if (this.sphereStates.length >= NUM_CLIPPING_PLANES) {
511
+ const faces = [
512
+ this.sphereStates[SPHEREINDEX.XMIN],
513
+ this.sphereStates[SPHEREINDEX.XMAX],
514
+ this.sphereStates[SPHEREINDEX.YMIN],
515
+ this.sphereStates[SPHEREINDEX.YMAX],
516
+ this.sphereStates[SPHEREINDEX.ZMIN],
517
+ this.sphereStates[SPHEREINDEX.ZMAX],
518
+ ];
519
+ rotationCenter = [
520
+ (faces[0].point[0] +
521
+ faces[1].point[0] +
522
+ faces[2].point[0] +
523
+ faces[3].point[0] +
524
+ faces[4].point[0] +
525
+ faces[5].point[0]) /
526
+ NUM_CLIPPING_PLANES,
527
+ (faces[0].point[1] +
528
+ faces[1].point[1] +
529
+ faces[2].point[1] +
530
+ faces[3].point[1] +
531
+ faces[4].point[1] +
532
+ faces[5].point[1]) /
533
+ NUM_CLIPPING_PLANES,
534
+ (faces[0].point[2] +
535
+ faces[1].point[2] +
536
+ faces[2].point[2] +
537
+ faces[3].point[2] +
538
+ faces[4].point[2] +
539
+ faces[5].point[2]) /
540
+ NUM_CLIPPING_PLANES,
541
+ ];
542
+ }
543
+ else {
544
+ rotationCenter = [0, 0, 0];
545
+ }
546
+ const transformX = mat4.identity(new Float32Array(16));
547
+ mat4.translate(transformX, transformX, rotationCenter);
548
+ mat4.rotate(transformX, transformX, (angleX * Math.PI) / 180, forwardV);
549
+ mat4.translate(transformX, transformX, [
550
+ -rotationCenter[0],
551
+ -rotationCenter[1],
552
+ -rotationCenter[2],
553
+ ]);
554
+ const transformY = mat4.identity(new Float32Array(16));
555
+ mat4.translate(transformY, transformY, rotationCenter);
556
+ mat4.rotate(transformY, transformY, (angleY * Math.PI) / 180, rightV);
557
+ mat4.translate(transformY, transformY, [
558
+ -rotationCenter[0],
559
+ -rotationCenter[1],
560
+ -rotationCenter[2],
561
+ ]);
562
+ const transform = mat4.create();
563
+ mat4.multiply(transform, transformY, transformX);
564
+ const normalTransformX4 = mat4.identity(new Float32Array(16));
565
+ mat4.rotate(normalTransformX4, normalTransformX4, (angleX * Math.PI) / 180, forwardV);
566
+ const normalTransformX = mat3.create();
567
+ mat3.fromMat4(normalTransformX, normalTransformX4);
568
+ const normalTransformY4 = mat4.identity(new Float32Array(16));
569
+ mat4.rotate(normalTransformY4, normalTransformY4, (angleY * Math.PI) / 180, rightV);
570
+ const normalTransformY = mat3.create();
571
+ mat3.fromMat4(normalTransformY, normalTransformY4);
572
+ const normalTransform = mat3.create();
573
+ mat3.multiply(normalTransform, normalTransformY, normalTransformX);
574
+ for (let i = 0; i < this.originalClippingPlanes.length; ++i) {
575
+ const plane = this.originalClippingPlanes[i];
576
+ const originVec = vec3.fromValues(plane.origin[0], plane.origin[1], plane.origin[2]);
577
+ vec3.transformMat4(originVec, originVec, transform);
578
+ plane.origin = [originVec[0], originVec[1], originVec[2]];
579
+ const normalVec = vec3.fromValues(plane.normal[0], plane.normal[1], plane.normal[2]);
580
+ vec3.transformMat3(normalVec, normalVec, normalTransform);
581
+ vec3.normalize(normalVec, normalVec);
582
+ plane.normal = [normalVec[0], normalVec[1], normalVec[2]];
583
+ }
584
+ if (this.sphereStates.length >= NUM_CLIPPING_PLANES) {
585
+ this.sphereStates[SPHEREINDEX.XMIN].point = [
586
+ ...this.originalClippingPlanes[PLANEINDEX.XMIN].origin,
587
+ ];
588
+ this.sphereStates[SPHEREINDEX.XMAX].point = [
589
+ ...this.originalClippingPlanes[PLANEINDEX.XMAX].origin,
590
+ ];
591
+ this.sphereStates[SPHEREINDEX.YMIN].point = [
592
+ ...this.originalClippingPlanes[PLANEINDEX.YMIN].origin,
593
+ ];
594
+ this.sphereStates[SPHEREINDEX.YMAX].point = [
595
+ ...this.originalClippingPlanes[PLANEINDEX.YMAX].origin,
596
+ ];
597
+ this.sphereStates[SPHEREINDEX.ZMIN].point = [
598
+ ...this.originalClippingPlanes[PLANEINDEX.ZMIN].origin,
599
+ ];
600
+ this.sphereStates[SPHEREINDEX.ZMAX].point = [
601
+ ...this.originalClippingPlanes[PLANEINDEX.ZMAX].origin,
602
+ ];
603
+ [
604
+ SPHEREINDEX.XMIN,
605
+ SPHEREINDEX.XMAX,
606
+ SPHEREINDEX.YMIN,
607
+ SPHEREINDEX.YMAX,
608
+ SPHEREINDEX.ZMIN,
609
+ SPHEREINDEX.ZMAX,
610
+ ].forEach((idx) => {
611
+ const s = this.sphereStates[idx];
612
+ s.sphereSource.setCenter(...s.point);
613
+ s.sphereSource.modified();
614
+ });
615
+ const cornerIndices = [
616
+ SPHEREINDEX.XMIN_YMIN_ZMIN,
617
+ SPHEREINDEX.XMIN_YMIN_ZMAX,
618
+ SPHEREINDEX.XMIN_YMAX_ZMIN,
619
+ SPHEREINDEX.XMIN_YMAX_ZMAX,
620
+ SPHEREINDEX.XMAX_YMIN_ZMIN,
621
+ SPHEREINDEX.XMAX_YMIN_ZMAX,
622
+ SPHEREINDEX.XMAX_YMAX_ZMIN,
623
+ SPHEREINDEX.XMAX_YMAX_ZMAX,
624
+ ];
625
+ cornerIndices.forEach((idx) => {
626
+ const cornerState = this.sphereStates[idx];
627
+ if (cornerState) {
628
+ const cornerVec = vec3.fromValues(cornerState.point[0], cornerState.point[1], cornerState.point[2]);
629
+ vec3.transformMat4(cornerVec, cornerVec, transform);
630
+ cornerState.point = [
631
+ cornerVec[0],
632
+ cornerVec[1],
633
+ cornerVec[2],
634
+ ];
635
+ cornerState.sphereSource.setCenter(...cornerState.point);
636
+ cornerState.sphereSource.modified();
637
+ }
638
+ });
639
+ Object.values(this.edgeLines).forEach(({ source, key1, key2 }) => {
640
+ const state1 = this.sphereStates.find((s) => s.uid === `corner_${key1}`);
641
+ const state2 = this.sphereStates.find((s) => s.uid === `corner_${key2}`);
642
+ if (state1 && state2) {
643
+ const points = source.getPoints();
644
+ points.setPoint(0, state1.point[0], state1.point[1], state1.point[2]);
645
+ points.setPoint(1, state2.point[0], state2.point[1], state2.point[2]);
646
+ points.modified();
647
+ source.modified();
648
+ }
649
+ });
650
+ }
651
+ this._updateClippingPlanes(viewport);
652
+ viewport.render();
653
+ triggerEvent(eventTarget, Events.VOLUMECROPPING_TOOL_CHANGED, {
654
+ originalClippingPlanes: this.originalClippingPlanes,
655
+ viewportId: viewport.id,
656
+ renderingEngineId: viewport.renderingEngineId,
657
+ seriesInstanceUID: this.seriesInstanceUID,
658
+ });
659
+ }
660
+ };
535
661
  this._rotateCamera = (viewport, centerWorld, axis, angle) => {
536
662
  const vtkCamera = viewport.getVtkActiveCamera();
537
663
  const viewUp = vtkCamera.getViewUp();
@@ -563,59 +689,55 @@ class VolumeCroppingTool extends BaseTool {
563
689
  this.mouseDragCallback = this._dragCallback.bind(this);
564
690
  }
565
691
  onSetToolActive() {
566
- if (this.sphereStates && this.sphereStates.length > 0) {
567
- if (this.configuration.showHandles) {
568
- this.setHandlesVisible(false);
569
- this.setClippingPlanesVisible(false);
570
- }
571
- else {
572
- this.setHandlesVisible(true);
573
- this.setClippingPlanesVisible(true);
574
- }
575
- }
576
- else {
577
- const viewportsInfo = this._getViewportsInfo();
578
- const subscribeToElementResize = () => {
579
- viewportsInfo.forEach(({ viewportId, renderingEngineId }) => {
580
- if (!this._resizeObservers.has(viewportId)) {
581
- const { viewport } = getEnabledElementByIds(viewportId, renderingEngineId) || { viewport: null };
582
- if (!viewport) {
692
+ const viewportsInfo = this._getViewportsInfo();
693
+ const subscribeToElementResize = () => {
694
+ viewportsInfo.forEach(({ viewportId, renderingEngineId }) => {
695
+ if (!this._resizeObservers.has(viewportId)) {
696
+ const { viewport } = getEnabledElementByIds(viewportId, renderingEngineId) || { viewport: null };
697
+ if (!viewport) {
698
+ return;
699
+ }
700
+ const { element } = viewport;
701
+ const resizeObserver = new ResizeObserver(() => {
702
+ const element = getEnabledElementByIds(viewportId, renderingEngineId);
703
+ if (!element) {
583
704
  return;
584
705
  }
585
- const { element } = viewport;
586
- const resizeObserver = new ResizeObserver(() => {
587
- const element = getEnabledElementByIds(viewportId, renderingEngineId);
588
- if (!element) {
589
- return;
590
- }
591
- const { viewport } = element;
592
- const viewPresentation = viewport.getViewPresentation();
593
- viewport.resetCamera();
594
- viewport.setViewPresentation(viewPresentation);
595
- viewport.render();
596
- });
597
- resizeObserver.observe(element);
598
- this._resizeObservers.set(viewportId, resizeObserver);
599
- }
600
- });
601
- };
602
- subscribeToElementResize();
603
- this._viewportAddedListener = (evt) => {
604
- if (evt.detail.toolGroupId === this.toolGroupId) {
605
- subscribeToElementResize();
706
+ const { viewport } = element;
707
+ const viewPresentation = viewport.getViewPresentation();
708
+ viewport.resetCamera();
709
+ viewport.setViewPresentation(viewPresentation);
710
+ viewport.render();
711
+ });
712
+ resizeObserver.observe(element);
713
+ this._resizeObservers.set(viewportId, resizeObserver);
606
714
  }
607
- };
608
- eventTarget.addEventListener(Events.TOOLGROUP_VIEWPORT_ADDED, this._viewportAddedListener);
609
- this._unsubscribeToViewportNewVolumeSet(viewportsInfo);
610
- this._subscribeToViewportNewVolumeSet(viewportsInfo);
715
+ });
716
+ };
717
+ subscribeToElementResize();
718
+ this._viewportAddedListener = (evt) => {
719
+ if (evt.detail.toolGroupId === this.toolGroupId) {
720
+ subscribeToElementResize();
721
+ }
722
+ };
723
+ eventTarget.addEventListener(Events.TOOLGROUP_VIEWPORT_ADDED, this._viewportAddedListener);
724
+ this._unsubscribeToViewportNewVolumeSet(viewportsInfo);
725
+ this._subscribeToViewportNewVolumeSet(viewportsInfo);
726
+ if (this.sphereStates && this.sphereStates.length === 0) {
727
+ this.originalClippingPlanes = [];
611
728
  this._initialize3DViewports(viewportsInfo);
729
+ }
730
+ this.configuration.showClippingPlanes = false;
731
+ this.configuration.showHandles = false;
732
+ const viewport = this._getViewport();
733
+ if (viewport &&
734
+ this.originalClippingPlanes &&
735
+ this.originalClippingPlanes.length > 0) {
736
+ this._updateClippingPlanes(viewport);
612
737
  if (this.sphereStates && this.sphereStates.length > 0) {
613
- this.setHandlesVisible(true);
614
- }
615
- else {
616
- this.originalClippingPlanes = [];
617
- this._initialize3DViewports(viewportsInfo);
738
+ this._updateHandlesVisibility();
618
739
  }
740
+ viewport.render();
619
741
  }
620
742
  }
621
743
  onSetToolDisabled() {
@@ -633,30 +755,7 @@ class VolumeCroppingTool extends BaseTool {
633
755
  setHandlesVisible(visible) {
634
756
  this.configuration.showHandles = visible;
635
757
  if (visible) {
636
- this.sphereStates[SPHEREINDEX.XMIN].point[0] =
637
- this.originalClippingPlanes[PLANEINDEX.XMIN].origin[0];
638
- this.sphereStates[SPHEREINDEX.XMAX].point[0] =
639
- this.originalClippingPlanes[PLANEINDEX.XMAX].origin[0];
640
- this.sphereStates[SPHEREINDEX.YMIN].point[1] =
641
- this.originalClippingPlanes[PLANEINDEX.YMIN].origin[1];
642
- this.sphereStates[SPHEREINDEX.YMAX].point[1] =
643
- this.originalClippingPlanes[PLANEINDEX.YMAX].origin[1];
644
- this.sphereStates[SPHEREINDEX.ZMIN].point[2] =
645
- this.originalClippingPlanes[PLANEINDEX.ZMIN].origin[2];
646
- this.sphereStates[SPHEREINDEX.ZMAX].point[2] =
647
- this.originalClippingPlanes[PLANEINDEX.ZMAX].origin[2];
648
- [
649
- SPHEREINDEX.XMIN,
650
- SPHEREINDEX.XMAX,
651
- SPHEREINDEX.YMIN,
652
- SPHEREINDEX.YMAX,
653
- SPHEREINDEX.ZMIN,
654
- SPHEREINDEX.ZMAX,
655
- ].forEach((idx) => {
656
- const s = this.sphereStates[idx];
657
- s.sphereSource.setCenter(...s.point);
658
- s.sphereSource.modified();
659
- });
758
+ this._updateFaceSpheresFromClippingPlanes();
660
759
  this._updateCornerSpheres();
661
760
  }
662
761
  this._updateHandlesVisibility();
@@ -676,14 +775,38 @@ class VolumeCroppingTool extends BaseTool {
676
775
  this.configuration.showClippingPlanes = visible;
677
776
  const viewport = this._getViewport();
678
777
  this._updateClippingPlanes(viewport);
778
+ if (this.sphereStates && this.sphereStates.length > 0) {
779
+ this.configuration.showHandles = visible;
780
+ this._updateHandlesVisibility();
781
+ }
782
+ if (visible &&
783
+ viewport &&
784
+ this.originalClippingPlanes?.length >= NUM_CLIPPING_PLANES) {
785
+ this._notifyClippingPlanesChanged(viewport);
786
+ }
679
787
  viewport.render();
680
788
  }
789
+ getRotatePlanesOnDrag() {
790
+ return this.rotatePlanesOnDrag;
791
+ }
792
+ setRotatePlanesOnDrag(enable) {
793
+ this.rotatePlanesOnDrag = enable;
794
+ const viewport = this._getViewport();
795
+ if (viewport) {
796
+ viewport.render();
797
+ }
798
+ }
681
799
  _dragCallback(evt) {
682
800
  const { element, currentPoints, lastPoints } = evt.detail;
683
801
  if (this.draggingSphereIndex !== null) {
684
802
  this._onMouseMoveSphere(evt);
685
803
  }
686
804
  else {
805
+ const shiftKey = evt.detail.event?.shiftKey ?? false;
806
+ if (this.rotatePlanesOnDrag === true || shiftKey) {
807
+ this._rotateClippingPlanes(evt);
808
+ return;
809
+ }
687
810
  const currentPointsCanvas = currentPoints.canvas;
688
811
  const lastPointsCanvas = lastPoints.canvas;
689
812
  const { rotateIncrementDegrees } = this.configuration;
@@ -739,13 +862,6 @@ class VolumeCroppingTool extends BaseTool {
739
862
  }
740
863
  _updateClippingPlanes(viewport) {
741
864
  const actorEntry = viewport.getDefaultActor();
742
- if (!actorEntry || !actorEntry.actor) {
743
- if (!viewport._missingActorWarned) {
744
- console.warn('VolumeCroppingTool._updateClippingPlanes: No default actor found in viewport.');
745
- viewport._missingActorWarned = true;
746
- }
747
- return;
748
- }
749
865
  const actor = actorEntry.actor;
750
866
  const mapper = actor.getMapper();
751
867
  const matrix = actor.getMatrix();
@@ -797,35 +913,6 @@ class VolumeCroppingTool extends BaseTool {
797
913
  }
798
914
  });
799
915
  }
800
- _addLine3DBetweenPoints(viewport, point1, point2, color = [0.7, 0.7, 0.7], uid = '') {
801
- if (point1[0] === point2[0] &&
802
- point1[1] === point2[1] &&
803
- point1[2] === point2[2]) {
804
- return { actor: null, source: null };
805
- }
806
- const points = vtkPoints.newInstance();
807
- points.setNumberOfPoints(2);
808
- points.setPoint(0, point1[0], point1[1], point1[2]);
809
- points.setPoint(1, point2[0], point2[1], point2[2]);
810
- const lines = vtkCellArray.newInstance({ values: [2, 0, 1] });
811
- const polyData = vtkPolyData.newInstance();
812
- polyData.setPoints(points);
813
- polyData.setLines(lines);
814
- const mapper = vtkMapper.newInstance();
815
- mapper.setInputData(polyData);
816
- const actor = vtkActor.newInstance();
817
- actor.setMapper(mapper);
818
- actor.getProperty().setColor(...color);
819
- actor.getProperty().setLineWidth(0.5);
820
- actor.getProperty().setOpacity(1.0);
821
- actor.getProperty().setInterpolationToFlat();
822
- actor.getProperty().setAmbient(1.0);
823
- actor.getProperty().setDiffuse(0.0);
824
- actor.getProperty().setSpecular(0.0);
825
- actor.setVisibility(this.configuration.showHandles);
826
- viewport.addActor({ actor, uid });
827
- return { actor, source: polyData };
828
- }
829
916
  _addSphere(viewport, point, axis, position, cornerKey = null, adaptiveRadius) {
830
917
  const uid = cornerKey ? `corner_${cornerKey}` : `${axis}_${position}`;
831
918
  const sphereState = this.sphereStates.find((s) => s.uid === uid);
@@ -854,9 +941,10 @@ class VolumeCroppingTool extends BaseTool {
854
941
  color = sphereColors.CORONAL || [0.0, 1.0, 0.0];
855
942
  }
856
943
  const idx = this.sphereStates.findIndex((s) => s.uid === uid);
944
+ const pointCopy = [point[0], point[1], point[2]];
857
945
  if (idx === -1) {
858
946
  this.sphereStates.push({
859
- point: point.slice(),
947
+ point: pointCopy,
860
948
  axis,
861
949
  uid,
862
950
  sphereSource,
@@ -866,88 +954,144 @@ class VolumeCroppingTool extends BaseTool {
866
954
  });
867
955
  }
868
956
  else {
869
- this.sphereStates[idx].point = point.slice();
957
+ this.sphereStates[idx].point = pointCopy;
870
958
  this.sphereStates[idx].sphereSource = sphereSource;
871
959
  }
872
- const existingActors = viewport.getActors();
873
- const existing = existingActors.find((a) => a.uid === uid);
874
- if (existing) {
875
- return;
876
- }
877
960
  sphereActor.getProperty().setColor(color);
878
961
  sphereActor.setVisibility(this.configuration.showHandles);
879
962
  viewport.addActor({ actor: sphereActor, uid: uid });
880
963
  }
881
- _calculateAdaptiveSphereRadius(diagonal) {
882
- const baseRadius = this.configuration.sphereRadius !== undefined
883
- ? this.configuration.sphereRadius
884
- : 8;
885
- const scaleFactor = this.configuration.sphereRadiusScale || 0.01;
886
- const adaptiveRadius = diagonal * scaleFactor;
887
- const minRadius = this.configuration.minSphereRadius || 2;
888
- const maxRadius = this.configuration.maxSphereRadius || 50;
889
- return Math.max(minRadius, Math.min(maxRadius, adaptiveRadius));
964
+ _getDirectionVectorForAxis(axis) {
965
+ if (this.originalClippingPlanes &&
966
+ this.originalClippingPlanes.length >= NUM_CLIPPING_PLANES) {
967
+ switch (axis) {
968
+ case 'x':
969
+ return this.originalClippingPlanes[PLANEINDEX.XMIN]
970
+ .normal;
971
+ case 'y':
972
+ return this.originalClippingPlanes[PLANEINDEX.YMIN]
973
+ .normal;
974
+ case 'z':
975
+ return this.originalClippingPlanes[PLANEINDEX.ZMIN]
976
+ .normal;
977
+ default:
978
+ return [1, 0, 0];
979
+ }
980
+ }
981
+ if (!this.volumeDirectionVectors) {
982
+ if (axis === 'x')
983
+ return [1, 0, 0];
984
+ if (axis === 'y')
985
+ return [0, 1, 0];
986
+ if (axis === 'z')
987
+ return [0, 0, 1];
988
+ return [1, 0, 0];
989
+ }
990
+ switch (axis) {
991
+ case 'x':
992
+ return this.volumeDirectionVectors.xDir;
993
+ case 'y':
994
+ return this.volumeDirectionVectors.yDir;
995
+ case 'z':
996
+ return this.volumeDirectionVectors.zDir;
997
+ default:
998
+ return [1, 0, 0];
999
+ }
1000
+ }
1001
+ _getVolumeActor(viewport) {
1002
+ const vp = viewport || this._getViewport();
1003
+ return vp?.getDefaultActor()?.actor;
1004
+ }
1005
+ _getVolumeMapper(viewport) {
1006
+ const actor = this._getVolumeActor(viewport);
1007
+ return actor?.getMapper();
1008
+ }
1009
+ _applyClippingPlanesToMapper(mapper) {
1010
+ mapper.removeAllClippingPlanes();
1011
+ for (let i = 0; i < NUM_CLIPPING_PLANES; ++i) {
1012
+ const plane = vtkPlane.newInstance({
1013
+ origin: this.originalClippingPlanes[i].origin,
1014
+ normal: this.originalClippingPlanes[i].normal,
1015
+ });
1016
+ mapper.addClippingPlane(plane);
1017
+ }
890
1018
  }
891
1019
  _updateClippingPlanesFromFaceSpheres(viewport) {
892
1020
  const mapper = viewport.getDefaultActor().actor.getMapper();
893
- this.originalClippingPlanes[0].origin = [
1021
+ this.originalClippingPlanes[PLANEINDEX.XMIN].origin = [
894
1022
  ...this.sphereStates[SPHEREINDEX.XMIN].point,
895
1023
  ];
896
- this.originalClippingPlanes[1].origin = [
1024
+ this.originalClippingPlanes[PLANEINDEX.XMAX].origin = [
897
1025
  ...this.sphereStates[SPHEREINDEX.XMAX].point,
898
1026
  ];
899
- this.originalClippingPlanes[2].origin = [
1027
+ this.originalClippingPlanes[PLANEINDEX.YMIN].origin = [
900
1028
  ...this.sphereStates[SPHEREINDEX.YMIN].point,
901
1029
  ];
902
- this.originalClippingPlanes[3].origin = [
1030
+ this.originalClippingPlanes[PLANEINDEX.YMAX].origin = [
903
1031
  ...this.sphereStates[SPHEREINDEX.YMAX].point,
904
1032
  ];
905
- this.originalClippingPlanes[4].origin = [
1033
+ this.originalClippingPlanes[PLANEINDEX.ZMIN].origin = [
906
1034
  ...this.sphereStates[SPHEREINDEX.ZMIN].point,
907
1035
  ];
908
- this.originalClippingPlanes[5].origin = [
1036
+ this.originalClippingPlanes[PLANEINDEX.ZMAX].origin = [
909
1037
  ...this.sphereStates[SPHEREINDEX.ZMAX].point,
910
1038
  ];
911
- mapper.removeAllClippingPlanes();
912
- for (let i = 0; i < 6; ++i) {
913
- const origin = this.originalClippingPlanes[i].origin;
914
- const normal = this.originalClippingPlanes[i].normal;
915
- const plane = vtkPlane.newInstance({
916
- origin,
917
- normal,
918
- });
919
- mapper.addClippingPlane(plane);
920
- }
1039
+ this._applyClippingPlanesToMapper(mapper);
921
1040
  }
922
1041
  _updateCornerSpheresFromFaces() {
923
- const xMin = this.sphereStates[SPHEREINDEX.XMIN].point[0];
924
- const xMax = this.sphereStates[SPHEREINDEX.XMAX].point[0];
925
- const yMin = this.sphereStates[SPHEREINDEX.YMIN].point[1];
926
- const yMax = this.sphereStates[SPHEREINDEX.YMAX].point[1];
927
- const zMin = this.sphereStates[SPHEREINDEX.ZMIN].point[2];
928
- const zMax = this.sphereStates[SPHEREINDEX.ZMAX].point[2];
1042
+ const { xDir, yDir, zDir } = this._getDirectionVectors();
1043
+ if (!xDir || !yDir || !zDir)
1044
+ return;
1045
+ const faceXMin = this.sphereStates[SPHEREINDEX.XMIN].point;
1046
+ const faceXMax = this.sphereStates[SPHEREINDEX.XMAX].point;
1047
+ const faceYMin = this.sphereStates[SPHEREINDEX.YMIN].point;
1048
+ const faceYMax = this.sphereStates[SPHEREINDEX.YMAX].point;
1049
+ const faceZMin = this.sphereStates[SPHEREINDEX.ZMIN].point;
1050
+ const faceZMax = this.sphereStates[SPHEREINDEX.ZMAX].point;
929
1051
  const corners = [
930
- { key: 'XMIN_YMIN_ZMIN', pos: [xMin, yMin, zMin] },
931
- { key: 'XMIN_YMIN_ZMAX', pos: [xMin, yMin, zMax] },
932
- { key: 'XMIN_YMAX_ZMIN', pos: [xMin, yMax, zMin] },
933
- { key: 'XMIN_YMAX_ZMAX', pos: [xMin, yMax, zMax] },
934
- { key: 'XMAX_YMIN_ZMIN', pos: [xMax, yMin, zMin] },
935
- { key: 'XMAX_YMIN_ZMAX', pos: [xMax, yMin, zMax] },
936
- { key: 'XMAX_YMAX_ZMIN', pos: [xMax, yMax, zMin] },
937
- { key: 'XMAX_YMAX_ZMAX', pos: [xMax, yMax, zMax] },
1052
+ {
1053
+ key: 'XMIN_YMIN_ZMIN',
1054
+ pos: this._calculateCornerFromFaces(faceXMin, faceYMin, faceZMin, xDir, yDir, zDir),
1055
+ },
1056
+ {
1057
+ key: 'XMIN_YMIN_ZMAX',
1058
+ pos: this._calculateCornerFromFaces(faceXMin, faceYMin, faceZMax, xDir, yDir, zDir),
1059
+ },
1060
+ {
1061
+ key: 'XMIN_YMAX_ZMIN',
1062
+ pos: this._calculateCornerFromFaces(faceXMin, faceYMax, faceZMin, xDir, yDir, zDir),
1063
+ },
1064
+ {
1065
+ key: 'XMIN_YMAX_ZMAX',
1066
+ pos: this._calculateCornerFromFaces(faceXMin, faceYMax, faceZMax, xDir, yDir, zDir),
1067
+ },
1068
+ {
1069
+ key: 'XMAX_YMIN_ZMIN',
1070
+ pos: this._calculateCornerFromFaces(faceXMax, faceYMin, faceZMin, xDir, yDir, zDir),
1071
+ },
1072
+ {
1073
+ key: 'XMAX_YMIN_ZMAX',
1074
+ pos: this._calculateCornerFromFaces(faceXMax, faceYMin, faceZMax, xDir, yDir, zDir),
1075
+ },
1076
+ {
1077
+ key: 'XMAX_YMAX_ZMIN',
1078
+ pos: this._calculateCornerFromFaces(faceXMax, faceYMax, faceZMin, xDir, yDir, zDir),
1079
+ },
1080
+ {
1081
+ key: 'XMAX_YMAX_ZMAX',
1082
+ pos: this._calculateCornerFromFaces(faceXMax, faceYMax, faceZMax, xDir, yDir, zDir),
1083
+ },
938
1084
  ];
939
1085
  for (const corner of corners) {
940
- const state = this.sphereStates.find((s) => s.uid === `corner_${corner.key}`);
941
- if (state) {
942
- state.point[0] = corner.pos[0];
943
- state.point[1] = corner.pos[1];
944
- state.point[2] = corner.pos[2];
945
- state.sphereSource.setCenter(...state.point);
946
- state.sphereSource.modified();
1086
+ const stateIndex = this.sphereStates.findIndex((s) => s.uid === `corner_${corner.key}`);
1087
+ if (stateIndex !== -1) {
1088
+ this._updateSpherePosition(stateIndex, corner.pos);
947
1089
  }
948
1090
  }
949
1091
  }
950
1092
  _updateFaceSpheresFromCorners() {
1093
+ if (!this.volumeDirectionVectors)
1094
+ return;
951
1095
  const corners = [
952
1096
  this.sphereStates[SPHEREINDEX.XMIN_YMIN_ZMIN].point,
953
1097
  this.sphereStates[SPHEREINDEX.XMIN_YMIN_ZMAX].point,
@@ -958,93 +1102,100 @@ class VolumeCroppingTool extends BaseTool {
958
1102
  this.sphereStates[SPHEREINDEX.XMAX_YMAX_ZMIN].point,
959
1103
  this.sphereStates[SPHEREINDEX.XMAX_YMAX_ZMAX].point,
960
1104
  ];
961
- const xs = corners.map((p) => p[0]);
962
- const ys = corners.map((p) => p[1]);
963
- const zs = corners.map((p) => p[2]);
964
- const xMin = Math.min(...xs), xMax = Math.max(...xs);
965
- const yMin = Math.min(...ys), yMax = Math.max(...ys);
966
- const zMin = Math.min(...zs), zMax = Math.max(...zs);
967
- this.sphereStates[SPHEREINDEX.XMIN].point = [
968
- xMin,
969
- (yMin + yMax) / 2,
970
- (zMin + zMax) / 2,
971
- ];
972
- this.sphereStates[SPHEREINDEX.XMAX].point = [
973
- xMax,
974
- (yMin + yMax) / 2,
975
- (zMin + zMax) / 2,
976
- ];
977
- this.sphereStates[SPHEREINDEX.YMIN].point = [
978
- (xMin + xMax) / 2,
979
- yMin,
980
- (zMin + zMax) / 2,
981
- ];
982
- this.sphereStates[SPHEREINDEX.YMAX].point = [
983
- (xMin + xMax) / 2,
984
- yMax,
985
- (zMin + zMax) / 2,
986
- ];
987
- this.sphereStates[SPHEREINDEX.ZMIN].point = [
988
- (xMin + xMax) / 2,
989
- (yMin + yMax) / 2,
990
- zMin,
991
- ];
992
- this.sphereStates[SPHEREINDEX.ZMAX].point = [
993
- (xMin + xMax) / 2,
994
- (yMin + yMax) / 2,
995
- zMax,
996
- ];
997
- [
998
- SPHEREINDEX.XMIN,
999
- SPHEREINDEX.XMAX,
1000
- SPHEREINDEX.YMIN,
1001
- SPHEREINDEX.YMAX,
1002
- SPHEREINDEX.ZMIN,
1003
- SPHEREINDEX.ZMAX,
1004
- ].forEach((idx) => {
1005
- const s = this.sphereStates[idx];
1006
- s.sphereSource.setCenter(...s.point);
1007
- s.sphereSource.modified();
1008
- });
1105
+ const faceXMin = this._averagePoints([
1106
+ corners[0],
1107
+ corners[1],
1108
+ corners[2],
1109
+ corners[3],
1110
+ ]);
1111
+ const faceXMax = this._averagePoints([
1112
+ corners[4],
1113
+ corners[5],
1114
+ corners[6],
1115
+ corners[7],
1116
+ ]);
1117
+ const faceYMin = this._averagePoints([
1118
+ corners[0],
1119
+ corners[1],
1120
+ corners[4],
1121
+ corners[5],
1122
+ ]);
1123
+ const faceYMax = this._averagePoints([
1124
+ corners[2],
1125
+ corners[3],
1126
+ corners[6],
1127
+ corners[7],
1128
+ ]);
1129
+ const faceZMin = this._averagePoints([
1130
+ corners[0],
1131
+ corners[2],
1132
+ corners[4],
1133
+ corners[6],
1134
+ ]);
1135
+ const faceZMax = this._averagePoints([
1136
+ corners[1],
1137
+ corners[3],
1138
+ corners[5],
1139
+ corners[7],
1140
+ ]);
1141
+ this._updateSpherePosition(SPHEREINDEX.XMIN, faceXMin);
1142
+ this._updateSpherePosition(SPHEREINDEX.XMAX, faceXMax);
1143
+ this._updateSpherePosition(SPHEREINDEX.YMIN, faceYMin);
1144
+ this._updateSpherePosition(SPHEREINDEX.YMAX, faceYMax);
1145
+ this._updateSpherePosition(SPHEREINDEX.ZMIN, faceZMin);
1146
+ this._updateSpherePosition(SPHEREINDEX.ZMAX, faceZMax);
1009
1147
  }
1010
1148
  _updateCornerSpheres() {
1011
- const xMin = this.sphereStates[SPHEREINDEX.XMIN].point[0];
1012
- const xMax = this.sphereStates[SPHEREINDEX.XMAX].point[0];
1013
- const yMin = this.sphereStates[SPHEREINDEX.YMIN].point[1];
1014
- const yMax = this.sphereStates[SPHEREINDEX.YMAX].point[1];
1015
- const zMin = this.sphereStates[SPHEREINDEX.ZMIN].point[2];
1016
- const zMax = this.sphereStates[SPHEREINDEX.ZMAX].point[2];
1149
+ const { xDir, yDir, zDir } = this._getDirectionVectors();
1150
+ if (!xDir || !yDir || !zDir)
1151
+ return;
1152
+ const faceXMin = this.sphereStates[SPHEREINDEX.XMIN].point;
1153
+ const faceXMax = this.sphereStates[SPHEREINDEX.XMAX].point;
1154
+ const faceYMin = this.sphereStates[SPHEREINDEX.YMIN].point;
1155
+ const faceYMax = this.sphereStates[SPHEREINDEX.YMAX].point;
1156
+ const faceZMin = this.sphereStates[SPHEREINDEX.ZMIN].point;
1157
+ const faceZMax = this.sphereStates[SPHEREINDEX.ZMAX].point;
1017
1158
  const corners = [
1018
- { key: 'XMIN_YMIN_ZMIN', pos: [xMin, yMin, zMin] },
1019
- { key: 'XMIN_YMIN_ZMAX', pos: [xMin, yMin, zMax] },
1020
- { key: 'XMIN_YMAX_ZMIN', pos: [xMin, yMax, zMin] },
1021
- { key: 'XMIN_YMAX_ZMAX', pos: [xMin, yMax, zMax] },
1022
- { key: 'XMAX_YMIN_ZMIN', pos: [xMax, yMin, zMin] },
1023
- { key: 'XMAX_YMIN_ZMAX', pos: [xMax, yMin, zMax] },
1024
- { key: 'XMAX_YMAX_ZMIN', pos: [xMax, yMax, zMin] },
1025
- { key: 'XMAX_YMAX_ZMAX', pos: [xMax, yMax, zMax] },
1159
+ {
1160
+ key: 'XMIN_YMIN_ZMIN',
1161
+ pos: this._calculateCornerFromProjection(faceXMin, faceYMin, faceZMin, xDir, yDir, zDir),
1162
+ },
1163
+ {
1164
+ key: 'XMIN_YMIN_ZMAX',
1165
+ pos: this._calculateCornerFromProjection(faceXMin, faceYMin, faceZMax, xDir, yDir, zDir),
1166
+ },
1167
+ {
1168
+ key: 'XMIN_YMAX_ZMIN',
1169
+ pos: this._calculateCornerFromProjection(faceXMin, faceYMax, faceZMin, xDir, yDir, zDir),
1170
+ },
1171
+ {
1172
+ key: 'XMIN_YMAX_ZMAX',
1173
+ pos: this._calculateCornerFromProjection(faceXMin, faceYMax, faceZMax, xDir, yDir, zDir),
1174
+ },
1175
+ {
1176
+ key: 'XMAX_YMIN_ZMIN',
1177
+ pos: this._calculateCornerFromProjection(faceXMax, faceYMin, faceZMin, xDir, yDir, zDir),
1178
+ },
1179
+ {
1180
+ key: 'XMAX_YMIN_ZMAX',
1181
+ pos: this._calculateCornerFromProjection(faceXMax, faceYMin, faceZMax, xDir, yDir, zDir),
1182
+ },
1183
+ {
1184
+ key: 'XMAX_YMAX_ZMIN',
1185
+ pos: this._calculateCornerFromProjection(faceXMax, faceYMax, faceZMin, xDir, yDir, zDir),
1186
+ },
1187
+ {
1188
+ key: 'XMAX_YMAX_ZMAX',
1189
+ pos: this._calculateCornerFromProjection(faceXMax, faceYMax, faceZMax, xDir, yDir, zDir),
1190
+ },
1026
1191
  ];
1027
1192
  for (const corner of corners) {
1028
- const state = this.sphereStates.find((s) => s.uid === `corner_${corner.key}`);
1029
- if (state) {
1030
- state.point[0] = corner.pos[0];
1031
- state.point[1] = corner.pos[1];
1032
- state.point[2] = corner.pos[2];
1033
- state.sphereSource.setCenter(...state.point);
1034
- state.sphereSource.modified();
1193
+ const stateIndex = this.sphereStates.findIndex((s) => s.uid === `corner_${corner.key}`);
1194
+ if (stateIndex !== -1) {
1195
+ this._updateSpherePosition(stateIndex, corner.pos);
1035
1196
  }
1036
1197
  }
1037
- Object.values(this.edgeLines).forEach(({ source, key1, key2 }) => {
1038
- const state1 = this.sphereStates.find((s) => s.uid === `corner_${key1}`);
1039
- const state2 = this.sphereStates.find((s) => s.uid === `corner_${key2}`);
1040
- if (state1 && state2) {
1041
- const points = source.getPoints();
1042
- points.setPoint(0, state1.point[0], state1.point[1], state1.point[2]);
1043
- points.setPoint(1, state2.point[0], state2.point[1], state2.point[2]);
1044
- points.modified();
1045
- source.modified();
1046
- }
1047
- });
1198
+ this._updateEdgeLines();
1048
1199
  }
1049
1200
  _unsubscribeToViewportNewVolumeSet(viewportsInfo) {
1050
1201
  viewportsInfo.forEach(({ viewportId, renderingEngineId }) => {
@@ -1060,6 +1211,105 @@ class VolumeCroppingTool extends BaseTool {
1060
1211
  element.addEventListener(Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, this._onNewVolume);
1061
1212
  });
1062
1213
  }
1214
+ _getDirectionVectors() {
1215
+ const hasPlanes = this.originalClippingPlanes?.length >= NUM_CLIPPING_PLANES;
1216
+ return {
1217
+ xDir: hasPlanes
1218
+ ? this.originalClippingPlanes[PLANEINDEX.XMIN].normal
1219
+ : this.volumeDirectionVectors?.xDir || [1, 0, 0],
1220
+ yDir: hasPlanes
1221
+ ? this.originalClippingPlanes[PLANEINDEX.YMIN].normal
1222
+ : this.volumeDirectionVectors?.yDir || [0, 1, 0],
1223
+ zDir: hasPlanes
1224
+ ? this.originalClippingPlanes[PLANEINDEX.ZMIN].normal
1225
+ : this.volumeDirectionVectors?.zDir || [0, 0, 1],
1226
+ };
1227
+ }
1228
+ _updateSpherePosition(sphereIndex, newPoint) {
1229
+ const state = this.sphereStates[sphereIndex];
1230
+ if (state) {
1231
+ state.point = newPoint;
1232
+ state.sphereSource.setCenter(...newPoint);
1233
+ state.sphereSource.modified();
1234
+ }
1235
+ }
1236
+ _updateFaceSpheresFromClippingPlanes() {
1237
+ const faceMappings = [
1238
+ { sphereIdx: SPHEREINDEX.XMIN, planeIdx: PLANEINDEX.XMIN },
1239
+ { sphereIdx: SPHEREINDEX.XMAX, planeIdx: PLANEINDEX.XMAX },
1240
+ { sphereIdx: SPHEREINDEX.YMIN, planeIdx: PLANEINDEX.YMIN },
1241
+ { sphereIdx: SPHEREINDEX.YMAX, planeIdx: PLANEINDEX.YMAX },
1242
+ { sphereIdx: SPHEREINDEX.ZMIN, planeIdx: PLANEINDEX.ZMIN },
1243
+ { sphereIdx: SPHEREINDEX.ZMAX, planeIdx: PLANEINDEX.ZMAX },
1244
+ ];
1245
+ faceMappings.forEach(({ sphereIdx, planeIdx }) => {
1246
+ const newPoint = [
1247
+ ...this.originalClippingPlanes[planeIdx].origin,
1248
+ ];
1249
+ this._updateSpherePosition(sphereIdx, newPoint);
1250
+ });
1251
+ }
1252
+ _averagePoints(points) {
1253
+ const sum = points.reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1], acc[2] + p[2]], [0, 0, 0]);
1254
+ return [
1255
+ sum[0] / points.length,
1256
+ sum[1] / points.length,
1257
+ sum[2] / points.length,
1258
+ ];
1259
+ }
1260
+ _notifyClippingPlanesChanged(viewport) {
1261
+ const eventData = {
1262
+ originalClippingPlanes: this.originalClippingPlanes,
1263
+ seriesInstanceUID: this.seriesInstanceUID,
1264
+ };
1265
+ if (viewport) {
1266
+ eventData.viewportId = viewport.id;
1267
+ eventData.renderingEngineId = viewport.renderingEngineId;
1268
+ }
1269
+ triggerEvent(eventTarget, Events.VOLUMECROPPING_TOOL_CHANGED, eventData);
1270
+ }
1271
+ _calculateCornerFromFaces(faceX, faceY, faceZ, xDir, yDir, zDir) {
1272
+ const deltaXY = [
1273
+ faceY[0] - faceX[0],
1274
+ faceY[1] - faceX[1],
1275
+ faceY[2] - faceX[2],
1276
+ ];
1277
+ const distY = deltaXY[0] * yDir[0] + deltaXY[1] * yDir[1] + deltaXY[2] * yDir[2];
1278
+ const deltaXZ = [
1279
+ faceZ[0] - faceX[0],
1280
+ faceZ[1] - faceX[1],
1281
+ faceZ[2] - faceX[2],
1282
+ ];
1283
+ const distZ = deltaXZ[0] * zDir[0] + deltaXZ[1] * zDir[1] + deltaXZ[2] * zDir[2];
1284
+ return [
1285
+ faceX[0] + distY * yDir[0] + distZ * zDir[0],
1286
+ faceX[1] + distY * yDir[1] + distZ * zDir[1],
1287
+ faceX[2] + distY * yDir[2] + distZ * zDir[2],
1288
+ ];
1289
+ }
1290
+ _calculateCornerFromProjection(faceX, faceY, faceZ, xDir, yDir, zDir) {
1291
+ const dX = faceX[0] * xDir[0] + faceX[1] * xDir[1] + faceX[2] * xDir[2];
1292
+ const dY = faceY[0] * yDir[0] + faceY[1] * yDir[1] + faceY[2] * yDir[2];
1293
+ const dZ = faceZ[0] * zDir[0] + faceZ[1] * zDir[1] + faceZ[2] * zDir[2];
1294
+ return [
1295
+ dX * xDir[0] + dY * yDir[0] + dZ * zDir[0],
1296
+ dX * xDir[1] + dY * yDir[1] + dZ * zDir[1],
1297
+ dX * xDir[2] + dY * yDir[2] + dZ * zDir[2],
1298
+ ];
1299
+ }
1300
+ _updateEdgeLines() {
1301
+ Object.values(this.edgeLines).forEach(({ source, key1, key2 }) => {
1302
+ const state1 = this.sphereStates.find((s) => s.uid === `corner_${key1}`);
1303
+ const state2 = this.sphereStates.find((s) => s.uid === `corner_${key2}`);
1304
+ if (state1 && state2) {
1305
+ const points = source.getPoints();
1306
+ points.setPoint(0, state1.point[0], state1.point[1], state1.point[2]);
1307
+ points.setPoint(1, state2.point[0], state2.point[1], state2.point[2]);
1308
+ points.modified();
1309
+ source.modified();
1310
+ }
1311
+ });
1312
+ }
1063
1313
  }
1064
1314
  VolumeCroppingTool.toolName = 'VolumeCropping';
1065
1315
  export default VolumeCroppingTool;