@cornerstonejs/tools 4.21.6 → 4.21.8

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.
@@ -4,6 +4,8 @@ declare class OrientationControllerTool extends BaseTool {
4
4
  private widget;
5
5
  private resizeObservers;
6
6
  private cameraHandlers;
7
+ private animationFrameHandles;
8
+ private animationTokens;
7
9
  constructor(toolProps?: {}, defaultToolProps?: {
8
10
  supportedInteractionTypes: string[];
9
11
  configuration: {
@@ -84,6 +84,8 @@ class OrientationControllerTool extends BaseTool {
84
84
  this.widget = new vtkOrientationControllerWidget();
85
85
  this.resizeObservers = new Map();
86
86
  this.cameraHandlers = new Map();
87
+ this.animationFrameHandles = new Map();
88
+ this.animationTokens = new Map();
87
89
  this._getViewportsInfo = () => {
88
90
  const viewports = getToolGroup(this.toolGroupId)?.viewportsInfo;
89
91
  return viewports || [];
@@ -177,6 +179,7 @@ class OrientationControllerTool extends BaseTool {
177
179
  }
178
180
  const volumeViewport = viewport;
179
181
  this.widget.positionActors(volumeViewport, actors, this.getPositionConfig());
182
+ this.widget.syncOverlayViewport(viewportId, volumeViewport);
180
183
  viewport.render();
181
184
  };
182
185
  }
@@ -242,6 +245,9 @@ class OrientationControllerTool extends BaseTool {
242
245
  this.resizeObservers.forEach((observer) => observer.disconnect());
243
246
  this.resizeObservers.clear();
244
247
  this.cameraHandlers.clear();
248
+ this.animationFrameHandles.forEach((handle) => cancelAnimationFrame(handle));
249
+ this.animationFrameHandles.clear();
250
+ this.animationTokens.clear();
245
251
  }
246
252
  createAnnotatedRhombActor() {
247
253
  const faceColors = this.getFaceColors();
@@ -255,6 +261,9 @@ class OrientationControllerTool extends BaseTool {
255
261
  });
256
262
  }
257
263
  addMarkerToViewport(viewportId, renderingEngineId) {
264
+ if (this.widget.getActors(viewportId)) {
265
+ return;
266
+ }
258
267
  const enabledElement = getEnabledElementByIds(viewportId, renderingEngineId);
259
268
  if (!enabledElement) {
260
269
  console.warn('OrientationControllerTool: No enabled element found');
@@ -295,8 +304,8 @@ class OrientationControllerTool extends BaseTool {
295
304
  }
296
305
  },
297
306
  onFaceHover: (result) => {
298
- if (result && result.actorIndex !== 0) {
299
- this.widget.highlightFace(result.pickedActor, result.cellId, volumeViewport, false);
307
+ if (result) {
308
+ this.widget.highlightFace(result.pickedActor, result.cellId, volumeViewport, result.actorIndex === 0);
300
309
  }
301
310
  else {
302
311
  this.widget.clearHighlight();
@@ -335,6 +344,14 @@ class OrientationControllerTool extends BaseTool {
335
344
  viewport.render();
336
345
  }
337
346
  animateCameraToOrientation(viewport, targetViewPlaneNormal, targetViewUp) {
347
+ const viewportId = viewport.id;
348
+ const existingHandle = this.animationFrameHandles.get(viewportId);
349
+ if (existingHandle !== undefined) {
350
+ cancelAnimationFrame(existingHandle);
351
+ this.animationFrameHandles.delete(viewportId);
352
+ }
353
+ const nextToken = (this.animationTokens.get(viewportId) ?? 0) + 1;
354
+ this.animationTokens.set(viewportId, nextToken);
338
355
  const keepOrientationUp = this.configuration.keepOrientationUp !== false;
339
356
  const renderer = viewport.getRenderer();
340
357
  const camera = renderer.getActiveCamera();
@@ -346,34 +363,33 @@ class OrientationControllerTool extends BaseTool {
346
363
  vec3.cross(startRight, startUp, startForward);
347
364
  vec3.normalize(startRight, startRight);
348
365
  const startMatrix = mat4.fromValues(startRight[0], startRight[1], startRight[2], 0, startUp[0], startUp[1], startUp[2], 0, startForward[0], startForward[1], startForward[2], 0, 0, 0, 0, 1);
366
+ const targetForward = vec3.normalize(vec3.create(), targetViewPlaneNormal);
349
367
  let targetUp;
350
368
  if (keepOrientationUp) {
351
369
  targetUp = vec3.fromValues(targetViewUp[0], targetViewUp[1], targetViewUp[2]);
352
370
  }
353
371
  else {
354
- const currentUp = vec3.normalize(vec3.create(), startUp);
355
- const normalizedForward = vec3.create();
356
- vec3.normalize(normalizedForward, targetViewPlaneNormal);
357
- const dot = vec3.dot(currentUp, normalizedForward);
372
+ const currentFwd = vec3.normalize(vec3.create(), startForward);
373
+ const rotQuat = quat.create();
374
+ quat.rotationTo(rotQuat, currentFwd, targetForward);
358
375
  targetUp = vec3.create();
359
- vec3.scaleAndAdd(targetUp, currentUp, normalizedForward, -dot);
376
+ vec3.transformQuat(targetUp, startUp, rotQuat);
360
377
  vec3.normalize(targetUp, targetUp);
361
- if (vec3.length(targetUp) < 0.001) {
362
- if (Math.abs(normalizedForward[2]) < 0.9) {
363
- targetUp = vec3.fromValues(0, 0, 1);
364
- }
365
- else {
366
- targetUp = vec3.fromValues(0, 1, 0);
367
- }
368
- const dot2 = vec3.dot(targetUp, normalizedForward);
369
- vec3.scaleAndAdd(targetUp, targetUp, normalizedForward, -dot2);
370
- vec3.normalize(targetUp, targetUp);
371
- }
372
378
  }
379
+ const upDotForward = vec3.dot(targetUp, targetForward);
380
+ vec3.scaleAndAdd(targetUp, targetUp, targetForward, -upDotForward);
381
+ if (vec3.length(targetUp) < 0.0001) {
382
+ targetUp = vec3.clone(startUp);
383
+ const fallbackDot = vec3.dot(targetUp, targetForward);
384
+ vec3.scaleAndAdd(targetUp, targetUp, targetForward, -fallbackDot);
385
+ }
386
+ vec3.normalize(targetUp, targetUp);
373
387
  const targetRight = vec3.create();
374
- vec3.cross(targetRight, targetUp, targetViewPlaneNormal);
388
+ vec3.cross(targetRight, targetUp, targetForward);
375
389
  vec3.normalize(targetRight, targetRight);
376
- const targetMatrix = mat4.fromValues(targetRight[0], targetRight[1], targetRight[2], 0, targetUp[0], targetUp[1], targetUp[2], 0, targetViewPlaneNormal[0], targetViewPlaneNormal[1], targetViewPlaneNormal[2], 0, 0, 0, 0, 1);
390
+ vec3.cross(targetUp, targetForward, targetRight);
391
+ vec3.normalize(targetUp, targetUp);
392
+ const targetMatrix = mat4.fromValues(targetRight[0], targetRight[1], targetRight[2], 0, targetUp[0], targetUp[1], targetUp[2], 0, targetForward[0], targetForward[1], targetForward[2], 0, 0, 0, 0, 1);
377
393
  const startQuat = mat4.getRotation(quat.create(), startMatrix);
378
394
  const targetQuat = mat4.getRotation(quat.create(), targetMatrix);
379
395
  let dotProduct = quat.dot(startQuat, targetQuat);
@@ -385,13 +401,21 @@ class OrientationControllerTool extends BaseTool {
385
401
  if (dotProduct > threshold) {
386
402
  return;
387
403
  }
388
- const steps = 10;
389
404
  const duration = 150;
390
- const stepDuration = duration / steps;
391
- let currentStep = 0;
392
- const animate = () => {
393
- currentStep++;
394
- const t = currentStep / steps;
405
+ const animationStart = performance.now();
406
+ const finalNormal = [
407
+ targetForward[0],
408
+ targetForward[1],
409
+ targetForward[2],
410
+ ];
411
+ const finalUp = [targetUp[0], targetUp[1], targetUp[2]];
412
+ const animate = (now) => {
413
+ if (this.animationTokens.get(viewportId) !== nextToken) {
414
+ return;
415
+ }
416
+ const elapsed = now - animationStart;
417
+ const t = Math.min(1, elapsed / duration);
418
+ const isLastStep = t >= 1;
395
419
  const easedT = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
396
420
  const interpolatedQuat = quat.create();
397
421
  quat.slerp(interpolatedQuat, startQuat, targetQuat, easedT);
@@ -400,16 +424,21 @@ class OrientationControllerTool extends BaseTool {
400
424
  const interpolatedForward = interpolatedMatrix.slice(8, 11);
401
425
  const interpolatedUp = interpolatedMatrix.slice(4, 7);
402
426
  viewport.setCamera({
403
- viewPlaneNormal: interpolatedForward,
404
- viewUp: interpolatedUp,
427
+ viewPlaneNormal: isLastStep ? finalNormal : interpolatedForward,
428
+ viewUp: isLastStep ? finalUp : interpolatedUp,
405
429
  });
406
430
  viewport.resetCamera(ANIMATE_RESET_CAMERA_OPTIONS);
407
431
  viewport.render();
408
- if (currentStep < steps) {
409
- setTimeout(animate, stepDuration);
432
+ if (!isLastStep) {
433
+ const handle = requestAnimationFrame(animate);
434
+ this.animationFrameHandles.set(viewportId, handle);
435
+ }
436
+ else {
437
+ this.animationFrameHandles.delete(viewportId);
410
438
  }
411
439
  };
412
- animate();
440
+ const handle = requestAnimationFrame(animate);
441
+ this.animationFrameHandles.set(viewportId, handle);
413
442
  }
414
443
  }
415
444
  export default OrientationControllerTool;
@@ -17,6 +17,7 @@ declare class VolumeCroppingTool extends BaseTool {
17
17
  originalClippingPlanes: ClippingPlane[];
18
18
  draggingSphereIndex: number | null;
19
19
  rotatePlanesOnDrag: boolean;
20
+ suppressPlaneRotationForCurrentDrag: boolean;
20
21
  cornerDragOffset: [number, number, number] | null;
21
22
  faceDragOffset: number | null;
22
23
  volumeDirectionVectors: {
@@ -50,6 +51,7 @@ declare class VolumeCroppingTool extends BaseTool {
50
51
  preMouseDownCallback: (evt: EventTypes.InteractionEventType) => boolean;
51
52
  setHandlesVisible(visible: boolean): void;
52
53
  getHandlesVisible(): any;
54
+ setHandleRadius(radius: number): void;
53
55
  getClippingPlanesVisible(): any;
54
56
  setClippingPlanesVisible(visible: boolean): void;
55
57
  getRotatePlanesOnDrag(): boolean;
@@ -11,6 +11,7 @@ import { getToolGroup } from '../store/ToolGroupManager';
11
11
  import { Events } from '../enums';
12
12
  import { PLANEINDEX, SPHEREINDEX, NUM_CLIPPING_PLANES, extractVolumeDirectionVectors, parseCornerKey, copyClippingPlanes, } from '../utilities/volumeCropping';
13
13
  import { addLine3DBetweenPoints, calculateAdaptiveSphereRadius, } from '../utilities/draw3D';
14
+ import { isDragOwnedBy } from '../utilities/interactionDragCoordinator';
14
15
  class VolumeCroppingTool extends BaseTool {
15
16
  constructor(toolProps = {}, defaultToolProps = {
16
17
  configuration: {
@@ -41,6 +42,7 @@ class VolumeCroppingTool extends BaseTool {
41
42
  this.originalClippingPlanes = [];
42
43
  this.draggingSphereIndex = null;
43
44
  this.rotatePlanesOnDrag = false;
45
+ this.suppressPlaneRotationForCurrentDrag = false;
44
46
  this.cornerDragOffset = null;
45
47
  this.faceDragOffset = null;
46
48
  this.volumeDirectionVectors = null;
@@ -63,6 +65,7 @@ class VolumeCroppingTool extends BaseTool {
63
65
  const { element } = eventDetail;
64
66
  const enabledElement = getEnabledElement(element);
65
67
  const { viewport } = enabledElement;
68
+ this.suppressPlaneRotationForCurrentDrag = isDragOwnedBy(viewport.id, 'orientation-controller');
66
69
  const actorEntry = viewport.getDefaultActor();
67
70
  const actor = actorEntry.actor;
68
71
  const mapper = actor.getMapper();
@@ -135,6 +138,7 @@ class VolumeCroppingTool extends BaseTool {
135
138
  this.draggingSphereIndex = null;
136
139
  this.cornerDragOffset = null;
137
140
  this.faceDragOffset = null;
141
+ this.suppressPlaneRotationForCurrentDrag = false;
138
142
  viewport.render();
139
143
  this._hasResolutionChanged = false;
140
144
  };
@@ -768,6 +772,23 @@ class VolumeCroppingTool extends BaseTool {
768
772
  getHandlesVisible() {
769
773
  return this.configuration.showHandles;
770
774
  }
775
+ setHandleRadius(radius) {
776
+ this.configuration.sphereRadius = radius;
777
+ this.sphereStates.forEach((state) => {
778
+ if (state?.sphereSource?.setRadius) {
779
+ state.sphereSource.setRadius(radius);
780
+ state.sphereSource.modified();
781
+ }
782
+ });
783
+ const viewportsInfo = this._getViewportsInfo();
784
+ const [viewport3D] = viewportsInfo;
785
+ if (!viewport3D) {
786
+ return;
787
+ }
788
+ const renderingEngine = getRenderingEngine(viewport3D.renderingEngineId);
789
+ const viewport = renderingEngine?.getViewport(viewport3D.viewportId);
790
+ viewport?.render();
791
+ }
771
792
  getClippingPlanesVisible() {
772
793
  return this.configuration.showClippingPlanes;
773
794
  }
@@ -803,7 +824,8 @@ class VolumeCroppingTool extends BaseTool {
803
824
  }
804
825
  else {
805
826
  const shiftKey = evt.detail.event?.shiftKey ?? false;
806
- if (this.rotatePlanesOnDrag === true || shiftKey) {
827
+ if ((this.rotatePlanesOnDrag === true || shiftKey) &&
828
+ !this.suppressPlaneRotationForCurrentDrag) {
807
829
  this._rotateClippingPlanes(evt);
808
830
  return;
809
831
  }
@@ -0,0 +1,5 @@
1
+ type DragOwner = 'orientation-controller';
2
+ export declare function beginOwnedDrag(viewportId: string, owner: DragOwner): boolean;
3
+ export declare function endOwnedDrag(viewportId: string, owner: DragOwner): void;
4
+ export declare function isDragOwnedBy(viewportId: string, owner: DragOwner): boolean;
5
+ export {};
@@ -0,0 +1,16 @@
1
+ const dragOwnerByViewportId = new Map();
2
+ export function beginOwnedDrag(viewportId, owner) {
3
+ if (dragOwnerByViewportId.has(viewportId)) {
4
+ return false;
5
+ }
6
+ dragOwnerByViewportId.set(viewportId, owner);
7
+ return true;
8
+ }
9
+ export function endOwnedDrag(viewportId, owner) {
10
+ if (dragOwnerByViewportId.get(viewportId) === owner) {
11
+ dragOwnerByViewportId.delete(viewportId);
12
+ }
13
+ }
14
+ export function isDragOwnedBy(viewportId, owner) {
15
+ return dragOwnerByViewportId.get(viewportId) === owner;
16
+ }
@@ -12,6 +12,10 @@ export const triggerWorkerProgress = (workerType, progress) => {
12
12
  };
13
13
  export const getSegmentationDataForWorker = (segmentationId, segmentIndices) => {
14
14
  const segmentation = getSegmentation(segmentationId);
15
+ if (!segmentation?.representationData) {
16
+ console.debug('getSegmentationDataForWorker: segmentation missing or not ready', segmentationId);
17
+ return null;
18
+ }
15
19
  const { representationData } = segmentation;
16
20
  const { Labelmap } = representationData;
17
21
  if (!Labelmap) {
@@ -35,14 +35,16 @@ export interface MouseHandlersCallbacks {
35
35
  export declare class vtkOrientationControllerWidget {
36
36
  private actors;
37
37
  private pickers;
38
+ private overlayRenderers;
39
+ private renderWindows;
38
40
  private highlightedFace;
39
41
  private mouseHandlers;
40
42
  createActors(config: OrientationControllerConfig): vtkActor[];
41
43
  addActorsToViewport(viewportId: string, viewport: Types.IVolumeViewport, actors: vtkActor[]): void;
42
- removeActorsFromViewport(viewportId: string, viewport: Types.IVolumeViewport): void;
44
+ removeActorsFromViewport(viewportId: string, _viewport: Types.IVolumeViewport): void;
43
45
  setupPicker(viewportId: string, actors: vtkActor[]): vtkCellPicker;
44
46
  pickAtPosition(evt: MouseEvent, viewportId: string, viewport: Types.IVolumeViewport, element: HTMLDivElement, actors: vtkActor[]): PickResult | null;
45
- calculateMarkerPosition(viewport: Types.IVolumeViewport, position: PositionConfig['position']): [number, number, number] | null;
47
+ calculateMarkerPosition(viewport: Types.IVolumeViewport, position: PositionConfig['position'], screenSizePixels: number): [number, number, number] | null;
46
48
  positionActors(viewport: Types.IVolumeViewport, actors: vtkActor[], config: PositionConfig): boolean;
47
49
  highlightFace(actor: vtkActor, cellId: number, viewport: Types.IVolumeViewport, isMainFace?: boolean): void;
48
50
  clearHighlight(): void;
@@ -50,7 +52,7 @@ export declare class vtkOrientationControllerWidget {
50
52
  cleanup: () => void;
51
53
  };
52
54
  getActors(viewportId: string): vtkActor[] | undefined;
53
- syncOverlayViewport(_viewportId: string, _viewport: Types.IVolumeViewport): void;
55
+ syncOverlayViewport(viewportId: string, viewport: Types.IVolumeViewport): void;
54
56
  getOrientationForFace(cellId: number): {
55
57
  viewPlaneNormal: number[];
56
58
  viewUp: number[];
@@ -1,11 +1,19 @@
1
1
  import vtkCellPicker from '@kitware/vtk.js/Rendering/Core/CellPicker';
2
2
  import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
3
+ import vtkCellArray from '@kitware/vtk.js/Common/Core/CellArray';
4
+ import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
5
+ import vtkPoints from '@kitware/vtk.js/Common/Core/Points';
6
+ import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData';
7
+ import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer';
3
8
  import { Enums } from '@cornerstonejs/core';
4
9
  import vtkAnnotatedRhombicuboctahedronActor from '../AnnotatedRhombicuboctahedronActor';
10
+ import { beginOwnedDrag, endOwnedDrag } from '../../interactionDragCoordinator';
5
11
  export class vtkOrientationControllerWidget {
6
12
  constructor() {
7
13
  this.actors = new Map();
8
14
  this.pickers = new Map();
15
+ this.overlayRenderers = new Map();
16
+ this.renderWindows = new Map();
9
17
  this.highlightedFace = null;
10
18
  this.mouseHandlers = new Map();
11
19
  }
@@ -83,19 +91,48 @@ export class vtkOrientationControllerWidget {
83
91
  if (existingActors) {
84
92
  this.removeActorsFromViewport(viewportId, viewport);
85
93
  }
86
- actors.forEach((actor, index) => {
87
- const uid = `orientation-controller-${viewportId}-${index}`;
88
- viewport.addActor({ actor, uid });
94
+ const renderWindow = viewport
95
+ .getRenderingEngine()
96
+ .getOffscreenMultiRenderWindow(viewport.id)
97
+ .getRenderWindow();
98
+ const mainRenderer = viewport
99
+ .getRenderingEngine()
100
+ ?.getRenderer(viewportId) ?? viewport.getRenderer();
101
+ const vtkMainRenderer = mainRenderer;
102
+ const overlayRenderer = vtkRenderer.newInstance();
103
+ overlayRenderer.setLayer(1);
104
+ overlayRenderer.setInteractive(false);
105
+ overlayRenderer.setPreserveColorBuffer(true);
106
+ overlayRenderer.setActiveCamera(vtkMainRenderer.getActiveCamera());
107
+ const vp = vtkMainRenderer.getViewport();
108
+ overlayRenderer.setViewport(...vp);
109
+ if (renderWindow.getNumberOfLayers() < 2) {
110
+ renderWindow.setNumberOfLayers(2);
111
+ }
112
+ renderWindow.addRenderer(overlayRenderer);
113
+ actors.forEach((actor) => {
114
+ overlayRenderer.addActor(actor);
89
115
  });
90
116
  this.actors.set(viewportId, actors);
117
+ this.overlayRenderers.set(viewportId, overlayRenderer);
118
+ this.renderWindows.set(viewportId, renderWindow);
91
119
  }
92
- removeActorsFromViewport(viewportId, viewport) {
120
+ removeActorsFromViewport(viewportId, _viewport) {
93
121
  const actors = this.actors.get(viewportId);
94
- if (actors) {
95
- const uids = actors.map((_, index) => `orientation-controller-${viewportId}-${index}`);
96
- viewport.removeActors(uids);
97
- this.actors.delete(viewportId);
122
+ const overlayRenderer = this.overlayRenderers.get(viewportId);
123
+ const renderWindow = this.renderWindows.get(viewportId);
124
+ if (actors && overlayRenderer) {
125
+ actors.forEach((actor) => {
126
+ overlayRenderer.removeActor(actor);
127
+ });
128
+ if (renderWindow) {
129
+ renderWindow.removeRenderer(overlayRenderer);
130
+ }
131
+ overlayRenderer.delete();
98
132
  }
133
+ this.actors.delete(viewportId);
134
+ this.overlayRenderers.delete(viewportId);
135
+ this.renderWindows.delete(viewportId);
99
136
  }
100
137
  setupPicker(viewportId, actors) {
101
138
  const picker = vtkCellPicker.newInstance({ opacityThreshold: 0.0001 });
@@ -113,10 +150,11 @@ export class vtkOrientationControllerWidget {
113
150
  if (!picker) {
114
151
  return null;
115
152
  }
116
- const mainRenderer = viewport
117
- .getRenderingEngine()
118
- ?.getRenderer(viewportId) ?? viewport.getRenderer();
119
- const renderer = mainRenderer;
153
+ const renderer = this.overlayRenderers.get(viewportId) ??
154
+ viewport
155
+ .getRenderingEngine()
156
+ ?.getRenderer(viewportId) ??
157
+ viewport.getRenderer();
120
158
  if (!renderer) {
121
159
  return null;
122
160
  }
@@ -151,7 +189,7 @@ export class vtkOrientationControllerWidget {
151
189
  }
152
190
  return null;
153
191
  }
154
- calculateMarkerPosition(viewport, position) {
192
+ calculateMarkerPosition(viewport, position, screenSizePixels) {
155
193
  const canvas = viewport.canvas;
156
194
  if (!canvas) {
157
195
  return null;
@@ -159,25 +197,30 @@ export class vtkOrientationControllerWidget {
159
197
  const devicePixelRatio = window.devicePixelRatio || 1;
160
198
  const canvasWidth = canvas.clientWidth || canvas.width / devicePixelRatio;
161
199
  const canvasHeight = canvas.clientHeight || canvas.height / devicePixelRatio;
162
- const cornerOffset = viewport.type === Enums.ViewportType.VOLUME_3D ? 55 : 35;
200
+ const marginRatio = viewport.type === Enums.ViewportType.VOLUME_3D ? 1.3 : 1.1;
201
+ const marginPxRaw = marginRatio * screenSizePixels;
202
+ const halfPx = screenSizePixels * 0.5;
203
+ const maxMarginX = Math.max(0, (canvasWidth - screenSizePixels) / 2);
204
+ const maxMarginY = Math.max(0, (canvasHeight - screenSizePixels) / 2);
205
+ const marginPx = Math.min(marginPxRaw, maxMarginX, maxMarginY);
163
206
  let canvasX;
164
207
  let canvasY;
165
208
  switch (position) {
166
209
  case 'top-left':
167
- canvasX = cornerOffset;
168
- canvasY = cornerOffset;
210
+ canvasX = marginPx + halfPx;
211
+ canvasY = marginPx + halfPx;
169
212
  break;
170
213
  case 'top-right':
171
- canvasX = canvasWidth - cornerOffset;
172
- canvasY = cornerOffset;
214
+ canvasX = canvasWidth - marginPx - halfPx;
215
+ canvasY = marginPx + halfPx;
173
216
  break;
174
217
  case 'bottom-left':
175
- canvasX = cornerOffset;
176
- canvasY = canvasHeight - cornerOffset;
218
+ canvasX = marginPx + halfPx;
219
+ canvasY = canvasHeight - marginPx - halfPx;
177
220
  break;
178
221
  default:
179
- canvasX = canvasWidth - cornerOffset;
180
- canvasY = canvasHeight - cornerOffset;
222
+ canvasX = canvasWidth - marginPx - halfPx;
223
+ canvasY = canvasHeight - marginPx - halfPx;
181
224
  }
182
225
  const canvasPos = [canvasX, canvasY];
183
226
  const worldPos = viewport.canvasToWorld(canvasPos);
@@ -207,7 +250,7 @@ export class vtkOrientationControllerWidget {
207
250
  const markerSize = screenSizePixels * worldUnitsPerPixel;
208
251
  actors.forEach((actor) => {
209
252
  actor.setScale(markerSize, markerSize, markerSize);
210
- const worldPos = this.calculateMarkerPosition(viewport, config.position);
253
+ const worldPos = this.calculateMarkerPosition(viewport, config.position, screenSizePixels);
211
254
  if (!worldPos) {
212
255
  console.warn('OrientationControllerWidget: Could not get world position');
213
256
  return;
@@ -226,6 +269,172 @@ export class vtkOrientationControllerWidget {
226
269
  }
227
270
  this.clearHighlight();
228
271
  if (isMainFace) {
272
+ const textureCollection = actor.getTextures?.();
273
+ const textureCandidate = Array.isArray(textureCollection)
274
+ ? textureCollection[0]
275
+ : textureCollection?.getItem?.(0);
276
+ const texture = textureCandidate;
277
+ const imageData = texture?.getInputData?.();
278
+ const scalars = imageData?.getPointData().getScalars();
279
+ const pixels = scalars?.getData();
280
+ const dims = imageData?.getDimensions();
281
+ if (!imageData ||
282
+ !scalars ||
283
+ !pixels ||
284
+ !dims ||
285
+ cellId < 0 ||
286
+ cellId > 5) {
287
+ const mapper = actor.getMapper();
288
+ const polyData = mapper.getInputData();
289
+ if (!polyData?.getCellPoints) {
290
+ return;
291
+ }
292
+ const { cellPointIds } = polyData.getCellPoints(cellId);
293
+ if (!cellPointIds || cellPointIds.length < 3) {
294
+ return;
295
+ }
296
+ const src = polyData.getPoints().getData();
297
+ const coords = [];
298
+ Array.from(cellPointIds).forEach((pid) => {
299
+ const o = pid * 3;
300
+ coords.push(src[o], src[o + 1], src[o + 2]);
301
+ });
302
+ const points = vtkPoints.newInstance();
303
+ points.setData(new Float32Array(coords), 3);
304
+ const polys = vtkCellArray.newInstance({
305
+ values: new Uint32Array([
306
+ cellPointIds.length,
307
+ ...Array.from(cellPointIds, (_, i) => i),
308
+ ]),
309
+ });
310
+ const poly = vtkPolyData.newInstance();
311
+ poly.setPoints(points);
312
+ poly.setPolys(polys);
313
+ const faceMapper = vtkMapper.newInstance();
314
+ faceMapper.setInputData(poly);
315
+ const faceActor = vtkActor.newInstance();
316
+ faceActor.setMapper(faceMapper);
317
+ const [sx, sy, sz] = actor.getScale();
318
+ const [px, py, pz] = actor.getPosition();
319
+ const [ox, oy, oz] = actor.getOrientation();
320
+ faceActor.setScale(sx, sy, sz);
321
+ faceActor.setPosition(px, py, pz);
322
+ faceActor.setOrientation(ox, oy, oz);
323
+ faceActor.setPickable(false);
324
+ const p = faceActor.getProperty();
325
+ p.setLighting(false);
326
+ p.setAmbient(1);
327
+ p.setDiffuse(0);
328
+ p.setColor(1, 1, 1);
329
+ p.setOpacity(0.58);
330
+ this.overlayRenderers.get(viewport.id)?.addActor(faceActor);
331
+ this.highlightedFace = {
332
+ actor,
333
+ cellId,
334
+ originalColor: [0, 0, 0, 0],
335
+ viewport,
336
+ isMainFace: true,
337
+ mainFaceHighlightActor: faceActor,
338
+ };
339
+ viewport.render();
340
+ return;
341
+ }
342
+ const [imageWidth, imageHeight] = dims;
343
+ const tileWidth = Math.floor(imageWidth / 3);
344
+ const tileHeight = Math.floor(imageHeight / 2);
345
+ const tileCol = cellId % 3;
346
+ const tileRow = Math.floor(cellId / 3);
347
+ const x0 = tileCol * tileWidth;
348
+ const y0 = tileRow * tileHeight;
349
+ const tileBackup = new Uint8Array(tileWidth * tileHeight * 4);
350
+ let b = 0;
351
+ for (let y = 0; y < tileHeight; y++) {
352
+ for (let x = 0; x < tileWidth; x++) {
353
+ const srcIndex = ((y0 + y) * imageWidth + (x0 + x)) * 4;
354
+ tileBackup[b++] = pixels[srcIndex];
355
+ tileBackup[b++] = pixels[srcIndex + 1];
356
+ tileBackup[b++] = pixels[srcIndex + 2];
357
+ tileBackup[b++] = pixels[srcIndex + 3];
358
+ }
359
+ }
360
+ const bgSampleIndices = [
361
+ ((y0 + 8) * imageWidth + (x0 + 8)) * 4,
362
+ ((y0 + 8) * imageWidth + (x0 + tileWidth - 9)) * 4,
363
+ ((y0 + tileHeight - 9) * imageWidth + (x0 + 8)) * 4,
364
+ ((y0 + tileHeight - 9) * imageWidth + (x0 + tileWidth - 9)) * 4,
365
+ ];
366
+ const bgColor = [0, 0, 0];
367
+ bgSampleIndices.forEach((idx) => {
368
+ bgColor[0] += pixels[idx];
369
+ bgColor[1] += pixels[idx + 1];
370
+ bgColor[2] += pixels[idx + 2];
371
+ });
372
+ bgColor[0] /= bgSampleIndices.length;
373
+ bgColor[1] /= bgSampleIndices.length;
374
+ bgColor[2] /= bgSampleIndices.length;
375
+ const glyphThreshold = 42;
376
+ const faceBrighten = 72;
377
+ const isGlyphPixel = (x, y) => {
378
+ if (x < 0 || x >= tileWidth || y < 0 || y >= tileHeight) {
379
+ return false;
380
+ }
381
+ const idx = ((y0 + y) * imageWidth + (x0 + x)) * 4;
382
+ const dr = pixels[idx] - bgColor[0];
383
+ const dg = pixels[idx + 1] - bgColor[1];
384
+ const db = pixels[idx + 2] - bgColor[2];
385
+ return Math.sqrt(dr * dr + dg * dg + db * db) >= glyphThreshold;
386
+ };
387
+ const borderWidth = Math.max(4, Math.floor(tileWidth * 0.035));
388
+ for (let y = 0; y < tileHeight; y++) {
389
+ for (let x = 0; x < tileWidth; x++) {
390
+ const onBorder = x < borderWidth ||
391
+ x >= tileWidth - borderWidth ||
392
+ y < borderWidth ||
393
+ y >= tileHeight - borderWidth;
394
+ if (onBorder || isGlyphPixel(x, y)) {
395
+ continue;
396
+ }
397
+ const idx = ((y0 + y) * imageWidth + (x0 + x)) * 4;
398
+ pixels[idx] = Math.min(255, pixels[idx] + faceBrighten);
399
+ pixels[idx + 1] = Math.min(255, pixels[idx + 1] + faceBrighten);
400
+ pixels[idx + 2] = Math.min(255, pixels[idx + 2] + faceBrighten);
401
+ }
402
+ }
403
+ for (let y = 0; y < tileHeight; y++) {
404
+ for (let x = 0; x < tileWidth; x++) {
405
+ const onBorder = x < borderWidth ||
406
+ x >= tileWidth - borderWidth ||
407
+ y < borderWidth ||
408
+ y >= tileHeight - borderWidth;
409
+ if (!onBorder) {
410
+ continue;
411
+ }
412
+ const idx = ((y0 + y) * imageWidth + (x0 + x)) * 4;
413
+ pixels[idx] = 0;
414
+ pixels[idx + 1] = 0;
415
+ pixels[idx + 2] = 0;
416
+ }
417
+ }
418
+ scalars.modified();
419
+ imageData.modified();
420
+ texture.modified?.();
421
+ actor.modified?.();
422
+ this.highlightedFace = {
423
+ actor,
424
+ cellId,
425
+ originalColor: [0, 0, 0, 0],
426
+ viewport,
427
+ isMainFace: true,
428
+ mainFaceTextureData: tileBackup,
429
+ mainFaceTile: {
430
+ x0,
431
+ y0,
432
+ width: tileWidth,
433
+ height: tileHeight,
434
+ imageWidth,
435
+ },
436
+ };
437
+ viewport.render();
229
438
  return;
230
439
  }
231
440
  const mapper = actor.getMapper();
@@ -266,9 +475,35 @@ export class vtkOrientationControllerWidget {
266
475
  return;
267
476
  }
268
477
  const { actor, cellId, originalColor, viewport, isMainFace } = this.highlightedFace;
269
- if (isMainFace && this.highlightedFace.originalScale) {
270
- const scale = this.highlightedFace.originalScale;
271
- actor.setScale(scale[0], scale[1], scale[2]);
478
+ if (isMainFace) {
479
+ const backup = this.highlightedFace.mainFaceTextureData;
480
+ const tile = this.highlightedFace.mainFaceTile;
481
+ const textures = actor.getTextures?.();
482
+ const texture = textures?.[0];
483
+ const imageData = texture?.getInputData?.();
484
+ const scalars = imageData?.getPointData().getScalars();
485
+ const pixels = scalars?.getData();
486
+ if (backup && tile && scalars && imageData && pixels) {
487
+ let b = 0;
488
+ for (let y = 0; y < tile.height; y++) {
489
+ for (let x = 0; x < tile.width; x++) {
490
+ const dstIndex = ((tile.y0 + y) * tile.imageWidth + (tile.x0 + x)) * 4;
491
+ pixels[dstIndex] = backup[b++];
492
+ pixels[dstIndex + 1] = backup[b++];
493
+ pixels[dstIndex + 2] = backup[b++];
494
+ pixels[dstIndex + 3] = backup[b++];
495
+ }
496
+ }
497
+ scalars.modified();
498
+ imageData.modified();
499
+ texture.modified?.();
500
+ actor.modified?.();
501
+ }
502
+ else if (this.highlightedFace.mainFaceHighlightActor) {
503
+ const overlayRenderer = this.overlayRenderers.get(viewport.id);
504
+ overlayRenderer?.removeActor(this.highlightedFace.mainFaceHighlightActor);
505
+ this.highlightedFace.mainFaceHighlightActor.delete();
506
+ }
272
507
  viewport.render();
273
508
  this.highlightedFace = null;
274
509
  return;
@@ -298,16 +533,25 @@ export class vtkOrientationControllerWidget {
298
533
  }
299
534
  setupMouseHandlers(viewportId, element, viewport, actors, callbacks) {
300
535
  let isMouseDown = false;
536
+ let didDrag = false;
537
+ let pendingPickResult = null;
538
+ let mouseDownCanvas = null;
539
+ const clickTolerancePx = 3;
301
540
  const hoverHandler = (evt) => {
302
541
  if (isMouseDown) {
542
+ if (mouseDownCanvas) {
543
+ const dx = evt.clientX - mouseDownCanvas.x;
544
+ const dy = evt.clientY - mouseDownCanvas.y;
545
+ if (dx * dx + dy * dy > clickTolerancePx * clickTolerancePx) {
546
+ didDrag = true;
547
+ }
548
+ }
303
549
  return;
304
550
  }
305
551
  const pickResult = this.pickAtPosition(evt, viewportId, viewport, element, actors);
306
552
  if (pickResult) {
307
553
  const { pickedActor, cellId, actorIndex } = pickResult;
308
- if (actorIndex !== 0) {
309
- this.highlightFace(pickedActor, cellId, viewport, false);
310
- }
554
+ this.highlightFace(pickedActor, cellId, viewport, actorIndex === 0);
311
555
  if (callbacks.onFaceHover) {
312
556
  callbacks.onFaceHover(pickResult);
313
557
  }
@@ -328,22 +572,33 @@ export class vtkOrientationControllerWidget {
328
572
  return;
329
573
  }
330
574
  isMouseDown = true;
331
- let globalCellId = pickResult.cellId;
332
- if (pickResult.actorIndex === 1) {
333
- globalCellId = pickResult.cellId + 6;
334
- }
335
- else if (pickResult.actorIndex === 2) {
336
- globalCellId = pickResult.cellId + 18;
337
- }
338
- callbacks.onFacePicked({
339
- ...pickResult,
340
- cellId: globalCellId,
341
- });
342
- evt.preventDefault();
343
- evt.stopPropagation();
575
+ didDrag = false;
576
+ pendingPickResult = pickResult;
577
+ mouseDownCanvas = { x: evt.clientX, y: evt.clientY };
578
+ beginOwnedDrag(viewportId, 'orientation-controller');
344
579
  };
345
- const mouseUpHandler = () => {
580
+ const mouseUpHandler = (evt) => {
581
+ if (isMouseDown && !didDrag && pendingPickResult) {
582
+ let globalCellId = pendingPickResult.cellId;
583
+ if (pendingPickResult.actorIndex === 1) {
584
+ globalCellId = pendingPickResult.cellId + 6;
585
+ }
586
+ else if (pendingPickResult.actorIndex === 2) {
587
+ globalCellId = pendingPickResult.cellId + 18;
588
+ }
589
+ callbacks.onFacePicked({
590
+ ...pendingPickResult,
591
+ cellId: globalCellId,
592
+ });
593
+ evt.preventDefault();
594
+ evt.stopImmediatePropagation();
595
+ evt.stopPropagation();
596
+ }
346
597
  isMouseDown = false;
598
+ didDrag = false;
599
+ pendingPickResult = null;
600
+ mouseDownCanvas = null;
601
+ endOwnedDrag(viewportId, 'orientation-controller');
347
602
  this.clearHighlight();
348
603
  };
349
604
  const dblclickHandler = (evt) => {
@@ -353,17 +608,18 @@ export class vtkOrientationControllerWidget {
353
608
  evt.stopImmediatePropagation();
354
609
  }
355
610
  };
356
- element.addEventListener('mousemove', hoverHandler);
357
- element.addEventListener('mousedown', clickHandler);
611
+ element.addEventListener('mousemove', hoverHandler, true);
612
+ element.addEventListener('mousedown', clickHandler, true);
358
613
  element.addEventListener('mouseup', mouseUpHandler);
359
614
  element.addEventListener('mouseleave', mouseUpHandler);
360
615
  element.addEventListener('dblclick', dblclickHandler, true);
361
616
  const cleanup = () => {
362
- element.removeEventListener('mousemove', hoverHandler);
363
- element.removeEventListener('mousedown', clickHandler);
617
+ element.removeEventListener('mousemove', hoverHandler, true);
618
+ element.removeEventListener('mousedown', clickHandler, true);
364
619
  element.removeEventListener('mouseup', mouseUpHandler);
365
620
  element.removeEventListener('mouseleave', mouseUpHandler);
366
621
  element.removeEventListener('dblclick', dblclickHandler, true);
622
+ endOwnedDrag(viewportId, 'orientation-controller');
367
623
  };
368
624
  this.mouseHandlers.set(viewportId, { cleanup });
369
625
  return { cleanup };
@@ -371,7 +627,19 @@ export class vtkOrientationControllerWidget {
371
627
  getActors(viewportId) {
372
628
  return this.actors.get(viewportId);
373
629
  }
374
- syncOverlayViewport(_viewportId, _viewport) {
630
+ syncOverlayViewport(viewportId, viewport) {
631
+ const overlayRenderer = this.overlayRenderers.get(viewportId);
632
+ if (!overlayRenderer) {
633
+ return;
634
+ }
635
+ const mainRenderer = viewport
636
+ .getRenderingEngine()
637
+ ?.getRenderer(viewportId) ?? viewport.getRenderer();
638
+ if (!mainRenderer) {
639
+ return;
640
+ }
641
+ const mainVp = mainRenderer.getViewport();
642
+ overlayRenderer.setViewport(...mainVp);
375
643
  }
376
644
  getOrientationForFace(cellId) {
377
645
  const orientations = new Map();
@@ -472,12 +740,31 @@ export class vtkOrientationControllerWidget {
472
740
  handler.cleanup();
473
741
  this.mouseHandlers.delete(viewportId);
474
742
  }
743
+ const overlayRenderer = this.overlayRenderers.get(viewportId);
744
+ const renderWindow = this.renderWindows.get(viewportId);
745
+ if (overlayRenderer) {
746
+ if (renderWindow) {
747
+ renderWindow.removeRenderer(overlayRenderer);
748
+ }
749
+ overlayRenderer.delete();
750
+ }
475
751
  this.actors.delete(viewportId);
476
752
  this.pickers.delete(viewportId);
753
+ this.overlayRenderers.delete(viewportId);
754
+ this.renderWindows.delete(viewportId);
477
755
  }
478
756
  else {
479
757
  this.mouseHandlers.forEach((handler) => handler.cleanup());
480
758
  this.mouseHandlers.clear();
759
+ this.overlayRenderers.forEach((overlayRenderer, vpId) => {
760
+ const renderWindow = this.renderWindows.get(vpId);
761
+ if (renderWindow) {
762
+ renderWindow.removeRenderer(overlayRenderer);
763
+ }
764
+ overlayRenderer.delete();
765
+ });
766
+ this.overlayRenderers.clear();
767
+ this.renderWindows.clear();
481
768
  this.actors.clear();
482
769
  this.pickers.clear();
483
770
  }
@@ -15,6 +15,7 @@ const EDGE_FACES = [
15
15
  11, 4, 5, 6, 22, 21, 4, 6, 7, 15, 14, 4, 7, 4, 17, 18, 4, 8, 11, 17, 16, 4, 9,
16
16
  20, 21, 10, 4, 13, 23, 22, 14, 4, 12, 19, 18, 15,
17
17
  ];
18
+ const FACE_HALF_SIZE = 0.792;
18
19
  function vtkRhombicuboctahedronSource(publicAPI, model) {
19
20
  model.classHierarchy.push('vtkRhombicuboctahedronSource');
20
21
  publicAPI.requestData = (inData, outData) => {
@@ -30,32 +31,31 @@ function vtkRhombicuboctahedronSource(publicAPI, model) {
30
31
  }
31
32
  }
32
33
  const phi = 1.4;
33
- const faceSize = 0.88;
34
34
  const vertices = [];
35
- vertices.push(-faceSize, -faceSize, -phi);
36
- vertices.push(faceSize, -faceSize, -phi);
37
- vertices.push(faceSize, faceSize, -phi);
38
- vertices.push(-faceSize, faceSize, -phi);
39
- vertices.push(-faceSize, -faceSize, phi);
40
- vertices.push(faceSize, -faceSize, phi);
41
- vertices.push(faceSize, faceSize, phi);
42
- vertices.push(-faceSize, faceSize, phi);
43
- vertices.push(-faceSize, -phi, -faceSize);
44
- vertices.push(faceSize, -phi, -faceSize);
45
- vertices.push(faceSize, -phi, faceSize);
46
- vertices.push(-faceSize, -phi, faceSize);
47
- vertices.push(-faceSize, phi, -faceSize);
48
- vertices.push(faceSize, phi, -faceSize);
49
- vertices.push(faceSize, phi, faceSize);
50
- vertices.push(-faceSize, phi, faceSize);
51
- vertices.push(-phi, -faceSize, -faceSize);
52
- vertices.push(-phi, -faceSize, faceSize);
53
- vertices.push(-phi, faceSize, faceSize);
54
- vertices.push(-phi, faceSize, -faceSize);
55
- vertices.push(phi, -faceSize, -faceSize);
56
- vertices.push(phi, -faceSize, faceSize);
57
- vertices.push(phi, faceSize, faceSize);
58
- vertices.push(phi, faceSize, -faceSize);
35
+ vertices.push(-FACE_HALF_SIZE, -FACE_HALF_SIZE, -phi);
36
+ vertices.push(FACE_HALF_SIZE, -FACE_HALF_SIZE, -phi);
37
+ vertices.push(FACE_HALF_SIZE, FACE_HALF_SIZE, -phi);
38
+ vertices.push(-FACE_HALF_SIZE, FACE_HALF_SIZE, -phi);
39
+ vertices.push(-FACE_HALF_SIZE, -FACE_HALF_SIZE, phi);
40
+ vertices.push(FACE_HALF_SIZE, -FACE_HALF_SIZE, phi);
41
+ vertices.push(FACE_HALF_SIZE, FACE_HALF_SIZE, phi);
42
+ vertices.push(-FACE_HALF_SIZE, FACE_HALF_SIZE, phi);
43
+ vertices.push(-FACE_HALF_SIZE, -phi, -FACE_HALF_SIZE);
44
+ vertices.push(FACE_HALF_SIZE, -phi, -FACE_HALF_SIZE);
45
+ vertices.push(FACE_HALF_SIZE, -phi, FACE_HALF_SIZE);
46
+ vertices.push(-FACE_HALF_SIZE, -phi, FACE_HALF_SIZE);
47
+ vertices.push(-FACE_HALF_SIZE, phi, -FACE_HALF_SIZE);
48
+ vertices.push(FACE_HALF_SIZE, phi, -FACE_HALF_SIZE);
49
+ vertices.push(FACE_HALF_SIZE, phi, FACE_HALF_SIZE);
50
+ vertices.push(-FACE_HALF_SIZE, phi, FACE_HALF_SIZE);
51
+ vertices.push(-phi, -FACE_HALF_SIZE, -FACE_HALF_SIZE);
52
+ vertices.push(-phi, -FACE_HALF_SIZE, FACE_HALF_SIZE);
53
+ vertices.push(-phi, FACE_HALF_SIZE, FACE_HALF_SIZE);
54
+ vertices.push(-phi, FACE_HALF_SIZE, -FACE_HALF_SIZE);
55
+ vertices.push(phi, -FACE_HALF_SIZE, -FACE_HALF_SIZE);
56
+ vertices.push(phi, -FACE_HALF_SIZE, FACE_HALF_SIZE);
57
+ vertices.push(phi, FACE_HALF_SIZE, FACE_HALF_SIZE);
58
+ vertices.push(phi, FACE_HALF_SIZE, -FACE_HALF_SIZE);
59
59
  let textureCoords = null;
60
60
  if (model.generate3DTextureCoordinates) {
61
61
  textureCoords = new Float64Array(24 * 3);
@@ -1 +1 @@
1
- export declare const version = "4.21.6";
1
+ export declare const version = "4.21.8";
@@ -1 +1 @@
1
- export const version = '4.21.6';
1
+ export const version = '4.21.8';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/tools",
3
- "version": "4.21.6",
3
+ "version": "4.21.8",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "types": "./dist/esm/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -105,11 +105,11 @@
105
105
  "lodash.get": "4.4.2"
106
106
  },
107
107
  "devDependencies": {
108
- "@cornerstonejs/core": "4.21.6",
108
+ "@cornerstonejs/core": "4.21.8",
109
109
  "canvas": "3.2.0"
110
110
  },
111
111
  "peerDependencies": {
112
- "@cornerstonejs/core": "4.21.6",
112
+ "@cornerstonejs/core": "4.21.8",
113
113
  "@kitware/vtk.js": "34.15.1",
114
114
  "@types/d3-array": "3.2.1",
115
115
  "@types/d3-interpolate": "3.0.4",
@@ -128,5 +128,5 @@
128
128
  "type": "individual",
129
129
  "url": "https://ohif.org/donate"
130
130
  },
131
- "gitHead": "f353e5f47d7535dd369b6a7e65ef12ef3f797153"
131
+ "gitHead": "65b9f66a24ac61dfa72640089ffde095dfff9d90"
132
132
  }