@cornerstonejs/core 1.69.0 → 1.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/cjs/RenderingEngine/BaseVolumeViewport.d.ts +3 -3
  2. package/dist/cjs/RenderingEngine/BaseVolumeViewport.js +36 -17
  3. package/dist/cjs/RenderingEngine/BaseVolumeViewport.js.map +1 -1
  4. package/dist/cjs/RenderingEngine/CanvasActor/index.js +1 -1
  5. package/dist/cjs/RenderingEngine/CanvasActor/index.js.map +1 -1
  6. package/dist/cjs/RenderingEngine/RenderingEngine.js +8 -26
  7. package/dist/cjs/RenderingEngine/RenderingEngine.js.map +1 -1
  8. package/dist/cjs/RenderingEngine/StackViewport.d.ts +5 -4
  9. package/dist/cjs/RenderingEngine/StackViewport.js +46 -24
  10. package/dist/cjs/RenderingEngine/StackViewport.js.map +1 -1
  11. package/dist/cjs/RenderingEngine/VideoViewport.d.ts +2 -2
  12. package/dist/cjs/RenderingEngine/VideoViewport.js +5 -9
  13. package/dist/cjs/RenderingEngine/VideoViewport.js.map +1 -1
  14. package/dist/cjs/RenderingEngine/Viewport.d.ts +17 -7
  15. package/dist/cjs/RenderingEngine/Viewport.js +173 -64
  16. package/dist/cjs/RenderingEngine/Viewport.js.map +1 -1
  17. package/dist/cjs/RenderingEngine/helpers/getOrCreateCanvas.d.ts +1 -0
  18. package/dist/cjs/RenderingEngine/helpers/getOrCreateCanvas.js +15 -2
  19. package/dist/cjs/RenderingEngine/helpers/getOrCreateCanvas.js.map +1 -1
  20. package/dist/cjs/cache/cache.js +0 -1
  21. package/dist/cjs/cache/cache.js.map +1 -1
  22. package/dist/cjs/loaders/volumeLoader.js +1 -1
  23. package/dist/cjs/loaders/volumeLoader.js.map +1 -1
  24. package/dist/cjs/types/IViewport.d.ts +18 -0
  25. package/dist/cjs/types/ViewportProperties.d.ts +1 -2
  26. package/dist/cjs/types/displayArea.d.ts +5 -1
  27. package/dist/cjs/types/index.d.ts +2 -2
  28. package/dist/cjs/utilities/index.d.ts +2 -1
  29. package/dist/cjs/utilities/index.js +3 -1
  30. package/dist/cjs/utilities/index.js.map +1 -1
  31. package/dist/cjs/utilities/loadImageToCanvas.d.ts +6 -2
  32. package/dist/cjs/utilities/loadImageToCanvas.js +14 -2
  33. package/dist/cjs/utilities/loadImageToCanvas.js.map +1 -1
  34. package/dist/cjs/utilities/renderToCanvasCPU.d.ts +2 -2
  35. package/dist/cjs/utilities/renderToCanvasCPU.js +1 -1
  36. package/dist/cjs/utilities/renderToCanvasCPU.js.map +1 -1
  37. package/dist/cjs/utilities/renderToCanvasGPU.d.ts +2 -2
  38. package/dist/cjs/utilities/renderToCanvasGPU.js +31 -12
  39. package/dist/cjs/utilities/renderToCanvasGPU.js.map +1 -1
  40. package/dist/esm/RenderingEngine/BaseVolumeViewport.js +37 -18
  41. package/dist/esm/RenderingEngine/BaseVolumeViewport.js.map +1 -1
  42. package/dist/esm/RenderingEngine/CanvasActor/index.js +1 -1
  43. package/dist/esm/RenderingEngine/CanvasActor/index.js.map +1 -1
  44. package/dist/esm/RenderingEngine/RenderingEngine.js +8 -26
  45. package/dist/esm/RenderingEngine/RenderingEngine.js.map +1 -1
  46. package/dist/esm/RenderingEngine/StackViewport.js +47 -25
  47. package/dist/esm/RenderingEngine/StackViewport.js.map +1 -1
  48. package/dist/esm/RenderingEngine/VideoViewport.js +5 -9
  49. package/dist/esm/RenderingEngine/VideoViewport.js.map +1 -1
  50. package/dist/esm/RenderingEngine/Viewport.js +173 -64
  51. package/dist/esm/RenderingEngine/Viewport.js.map +1 -1
  52. package/dist/esm/RenderingEngine/helpers/getOrCreateCanvas.js +14 -1
  53. package/dist/esm/RenderingEngine/helpers/getOrCreateCanvas.js.map +1 -1
  54. package/dist/esm/cache/cache.js +0 -1
  55. package/dist/esm/cache/cache.js.map +1 -1
  56. package/dist/esm/loaders/volumeLoader.js +1 -1
  57. package/dist/esm/loaders/volumeLoader.js.map +1 -1
  58. package/dist/esm/utilities/index.js +2 -1
  59. package/dist/esm/utilities/index.js.map +1 -1
  60. package/dist/esm/utilities/loadImageToCanvas.js +14 -2
  61. package/dist/esm/utilities/loadImageToCanvas.js.map +1 -1
  62. package/dist/esm/utilities/renderToCanvasCPU.js +1 -1
  63. package/dist/esm/utilities/renderToCanvasCPU.js.map +1 -1
  64. package/dist/esm/utilities/renderToCanvasGPU.js +8 -9
  65. package/dist/esm/utilities/renderToCanvasGPU.js.map +1 -1
  66. package/dist/types/RenderingEngine/BaseVolumeViewport.d.ts +3 -3
  67. package/dist/types/RenderingEngine/BaseVolumeViewport.d.ts.map +1 -1
  68. package/dist/types/RenderingEngine/CanvasActor/index.d.ts.map +1 -1
  69. package/dist/types/RenderingEngine/RenderingEngine.d.ts.map +1 -1
  70. package/dist/types/RenderingEngine/StackViewport.d.ts +5 -4
  71. package/dist/types/RenderingEngine/StackViewport.d.ts.map +1 -1
  72. package/dist/types/RenderingEngine/VideoViewport.d.ts +2 -2
  73. package/dist/types/RenderingEngine/VideoViewport.d.ts.map +1 -1
  74. package/dist/types/RenderingEngine/Viewport.d.ts +17 -7
  75. package/dist/types/RenderingEngine/Viewport.d.ts.map +1 -1
  76. package/dist/types/RenderingEngine/helpers/getOrCreateCanvas.d.ts +1 -0
  77. package/dist/types/RenderingEngine/helpers/getOrCreateCanvas.d.ts.map +1 -1
  78. package/dist/types/cache/cache.d.ts.map +1 -1
  79. package/dist/types/loaders/volumeLoader.d.ts.map +1 -1
  80. package/dist/types/types/IViewport.d.ts +18 -0
  81. package/dist/types/types/IViewport.d.ts.map +1 -1
  82. package/dist/types/types/ViewportProperties.d.ts +1 -2
  83. package/dist/types/types/ViewportProperties.d.ts.map +1 -1
  84. package/dist/types/types/displayArea.d.ts +5 -1
  85. package/dist/types/types/displayArea.d.ts.map +1 -1
  86. package/dist/types/types/index.d.ts +2 -2
  87. package/dist/types/types/index.d.ts.map +1 -1
  88. package/dist/types/utilities/index.d.ts +2 -1
  89. package/dist/types/utilities/index.d.ts.map +1 -1
  90. package/dist/types/utilities/loadImageToCanvas.d.ts +6 -2
  91. package/dist/types/utilities/loadImageToCanvas.d.ts.map +1 -1
  92. package/dist/types/utilities/renderToCanvasCPU.d.ts +2 -2
  93. package/dist/types/utilities/renderToCanvasCPU.d.ts.map +1 -1
  94. package/dist/types/utilities/renderToCanvasGPU.d.ts +2 -2
  95. package/dist/types/utilities/renderToCanvasGPU.d.ts.map +1 -1
  96. package/dist/umd/index.js +1 -1
  97. package/dist/umd/index.js.map +1 -1
  98. package/package.json +2 -2
  99. package/src/RenderingEngine/BaseVolumeViewport.ts +30 -6
  100. package/src/RenderingEngine/CanvasActor/index.ts +2 -1
  101. package/src/RenderingEngine/RenderingEngine.ts +11 -35
  102. package/src/RenderingEngine/StackViewport.ts +55 -14
  103. package/src/RenderingEngine/VideoViewport.ts +14 -9
  104. package/src/RenderingEngine/Viewport.ts +353 -91
  105. package/src/RenderingEngine/helpers/getOrCreateCanvas.ts +32 -1
  106. package/src/cache/cache.ts +2 -1
  107. package/src/loaders/volumeLoader.ts +2 -1
  108. package/src/types/IViewport.ts +130 -10
  109. package/src/types/ViewportProperties.ts +1 -3
  110. package/src/types/displayArea.ts +6 -1
  111. package/src/types/index.ts +4 -0
  112. package/src/utilities/index.ts +2 -0
  113. package/src/utilities/loadImageToCanvas.ts +38 -3
  114. package/src/utilities/renderToCanvasCPU.ts +7 -2
  115. package/src/utilities/renderToCanvasGPU.ts +20 -14
@@ -29,17 +29,21 @@ import type {
29
29
  FlipDirection,
30
30
  EventTypes,
31
31
  DisplayArea,
32
+ ViewPresentation,
33
+ ViewReference,
34
+ ViewportProperties,
32
35
  } from '../types';
33
36
  import type {
34
37
  ViewportInput,
35
38
  IViewport,
36
39
  ViewReferenceSpecifier,
37
- ViewReference,
38
40
  ReferenceCompatibleOptions,
41
+ ViewPresentationSelector,
39
42
  } from '../types/IViewport';
40
43
  import type { vtkSlabCamera } from './vtkClasses/vtkSlabCamera';
41
44
  import { getConfiguration } from '../init';
42
45
  import IImageCalibration from '../types/IImageCalibration';
46
+ import { InterpolationType } from '../enums';
43
47
 
44
48
  /**
45
49
  * An object representing a single viewport, which is a camera
@@ -51,6 +55,31 @@ import IImageCalibration from '../types/IImageCalibration';
51
55
  * logic.
52
56
  */
53
57
  class Viewport implements IViewport {
58
+ /**
59
+ * CameraViewPresentation is a view preentation selector that has all the
60
+ * camera related presentation selections, and would typically be used for
61
+ * choosing presentation information between two viewports showing the same
62
+ * type of orientation of a view, such as the CT, PT and fusion views in the
63
+ * same orientation view.
64
+ */
65
+ public static readonly CameraViewPresentation: ViewPresentationSelector = {
66
+ rotation: true,
67
+ pan: true,
68
+ zoom: true,
69
+ displayArea: true,
70
+ };
71
+
72
+ /**
73
+ * TransferViewPresentation is a view presentation selector that selects all
74
+ * the transfer function related attributes. It would typically be used for
75
+ * synchronizing different orientations of the same series, or for
76
+ * synchronizing two views of the same type of series such as a CT.
77
+ */
78
+ public static readonly TransferViewPresentation: ViewPresentationSelector = {
79
+ windowLevel: true,
80
+ paletteLut: true,
81
+ };
82
+
54
83
  /** unique identifier for the viewport */
55
84
  readonly id: string;
56
85
  /** HTML element in DOM that is used for rendering the viewport */
@@ -89,7 +118,7 @@ class Viewport implements IViewport {
89
118
  /** options for the viewport which includes orientation axis, backgroundColor and displayArea */
90
119
  options: ViewportInputOptions;
91
120
  /** informs if a new actor was added before a resetCameraClippingRange phase */
92
- private _suppressCameraModifiedEvents = false;
121
+ _suppressCameraModifiedEvents = false;
93
122
  /** A flag representing if viewport methods should fire events or not */
94
123
  readonly suppressEvents: boolean;
95
124
  protected hasPixelSpacing = true;
@@ -101,7 +130,7 @@ class Viewport implements IViewport {
101
130
  /** The camera that is defined for resetting displayArea to ensure absolute displayArea
102
131
  * settings
103
132
  */
104
- private fitToCanvasCamera: ICamera;
133
+ protected fitToCanvasCamera: ICamera;
105
134
 
106
135
  constructor(props: ViewportInput) {
107
136
  this.id = props.id;
@@ -135,9 +164,12 @@ class Viewport implements IViewport {
135
164
  worldToCanvas: (worldPos: Point3) => Point2;
136
165
  customRenderViewportToCanvas: () => unknown;
137
166
  resize: () => void;
138
- getProperties: () => void;
167
+ getProperties: () => ViewportProperties = () => ({});
139
168
  updateRenderingPipeline: () => void;
140
169
  getNumberOfSlices: () => number;
170
+ protected setRotation = (_rotation: number) => {
171
+ /*empty*/
172
+ };
141
173
 
142
174
  static get useCustomRenderingPipeline(): boolean {
143
175
  return false;
@@ -590,6 +622,13 @@ class Viewport implements IViewport {
590
622
  return intersections;
591
623
  }
592
624
 
625
+ /**
626
+ * Sets the interpolation type. No-op in the base.
627
+ */
628
+ protected setInterpolationType(_interpolationType: InterpolationType, _arg?) {
629
+ // No-op - just done to allow setting on the base viewport
630
+ }
631
+
593
632
  /**
594
633
  * Sets the camera to an initial bounds. If
595
634
  * resetPan and resetZoom are true it places the focal point at the center of
@@ -602,70 +641,43 @@ class Viewport implements IViewport {
602
641
  displayArea: DisplayArea,
603
642
  suppressEvents = false
604
643
  ): void {
605
- const { storeAsInitialCamera } = displayArea;
606
-
607
- // Setup the current camera as the fit to canvas camera as the one that is
608
- // used as the base for calculations, but it isn't final, so don't fire
609
- // events because the camera is still changing.
610
- this.setCameraNoEvent(this.fitToCanvasCamera);
611
-
612
- const { imageArea, imageCanvasPoint } = displayArea;
644
+ if (!displayArea) {
645
+ return;
646
+ }
647
+ const { storeAsInitialCamera, type: areaType } = displayArea;
613
648
 
614
- let zoom = 1;
615
- if (imageArea) {
616
- const [areaX, areaY] = imageArea;
617
- zoom = Math.min(this.getZoom() / areaX, this.getZoom() / areaY);
618
- // Don't set as initial camera because then the zoom interactions don't
619
- // work consistently.
620
- // TODO: Add a better method to handle initial camera
621
- this.setZoom(this.insetImageMultiplier * zoom);
649
+ // Instead of storing the camera itself, if initial camera is set,
650
+ // then store the display area as the baseline display area.
651
+ if (storeAsInitialCamera) {
652
+ this.options.displayArea = displayArea;
622
653
  }
623
654
 
624
- // getting the image info
625
- const imageData = this.getDefaultImageData();
626
- if (imageCanvasPoint && imageData) {
627
- const { imagePoint, canvasPoint } = imageCanvasPoint;
628
- const [canvasX, canvasY] = canvasPoint;
629
- const devicePixelRatio = window?.devicePixelRatio || 1;
630
- const validateCanvasPanX = this.sWidth / devicePixelRatio;
631
- const validateCanvasPanY = this.sHeight / devicePixelRatio;
632
- const canvasPanX = validateCanvasPanX * (canvasX - 0.5);
633
- const canvasPanY = validateCanvasPanY * (canvasY - 0.5);
634
- const dimensions = imageData.getDimensions();
635
- const canvasZero = this.worldToCanvas(imageData.indexToWorld([0, 0, 0]));
636
- const canvasEdge = this.worldToCanvas(
637
- imageData.indexToWorld([
638
- dimensions[0] - 1,
639
- dimensions[1] - 1,
640
- dimensions[2],
641
- ])
642
- );
643
- const canvasImage = [
644
- canvasEdge[0] - canvasZero[0],
645
- canvasEdge[1] - canvasZero[1],
646
- ];
647
- const [imgWidth, imgHeight] = canvasImage;
648
- const [imageX, imageY] = imagePoint;
649
- const imagePanX =
650
- (zoom * imgWidth * (0.5 - imageX) * validateCanvasPanY) / imgHeight;
651
- const imagePanY = zoom * validateCanvasPanY * (0.5 - imageY);
655
+ // make calculations relative to the fitToCanvasCamera view
656
+ const { _suppressCameraModifiedEvents } = this;
657
+ this._suppressCameraModifiedEvents = true;
652
658
 
653
- const newPositionX = imagePanX + canvasPanX;
654
- const newPositionY = imagePanY + canvasPanY;
659
+ // This should only apply for storeAsInitialCamera, but the calculations
660
+ // currently don't quite work otherwise.
661
+ // TODO - fix so that the store works for existing transforms
662
+ this.setCamera(this.fitToCanvasCamera);
655
663
 
656
- const deltaPoint2: Point2 = [newPositionX, newPositionY];
657
- // The pan is part of the display area settings, not the initial camera, so
658
- // don't store as initial camera here - that breaks rotation and other changes.
659
- this.setPan(deltaPoint2);
664
+ if (areaType === 'SCALE') {
665
+ this.setDisplayAreaScale(displayArea);
666
+ } else {
667
+ this.setInterpolationType(
668
+ this.getProperties().interpolationType || InterpolationType.LINEAR
669
+ );
670
+ this.setDisplayAreaFit(displayArea);
660
671
  }
661
672
 
662
- // Instead of storing the camera itself, if initial camera is set,
663
- // then store the display area as the baseline display area.
673
+ // Set the initial camera if appropriate
664
674
  if (storeAsInitialCamera) {
665
- this.options.displayArea = displayArea;
675
+ this.initialCamera = this.getCamera();
666
676
  }
667
677
 
668
- if (!suppressEvents) {
678
+ // Restore event firing
679
+ this._suppressCameraModifiedEvents = _suppressCameraModifiedEvents;
680
+ if (!suppressEvents && !_suppressCameraModifiedEvents) {
669
681
  const eventDetail: EventTypes.DisplayAreaModifiedEventDetail = {
670
682
  viewportId: this.id,
671
683
  displayArea: displayArea,
@@ -673,6 +685,156 @@ class Viewport implements IViewport {
673
685
  };
674
686
 
675
687
  triggerEvent(this.element, Events.DISPLAY_AREA_MODIFIED, eventDetail);
688
+ this.setCamera(this.getCamera());
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Sets the viewport to pixel scaling mode. Pixel scaling displays
694
+ * 1 image pixel as 1 (or scale) physical screen pixels. That is,
695
+ * a 1024x512 image will be displayed with scale=2, as 2048x1024
696
+ * physical image pixels.
697
+ *
698
+ * @param displayArea - display area to set
699
+ * * displayArea.scale - the number of physical pixels to display
700
+ * each image pixel in. Values `< 1` mean smaller than physical,
701
+ * while values `> 1` mean more than one pixel. Default is 1
702
+ * Suggest using whole numbers or integer fractions (eg `1/3`)
703
+ */
704
+ protected setDisplayAreaScale(displayArea: DisplayArea): void {
705
+ const { scale = 1 } = displayArea;
706
+ const canvas = this.canvas;
707
+ const height = canvas.height;
708
+ const width = canvas.width;
709
+ if (height < 8 || width < 8) {
710
+ return;
711
+ }
712
+ const imageData = this.getDefaultImageData();
713
+ const spacingWorld = imageData.getSpacing();
714
+ const spacing = spacingWorld[1];
715
+ // Need nearest interpolation for scale
716
+ this.setInterpolationType(InterpolationType.NEAREST);
717
+ this.setCamera({ parallelScale: (height * spacing) / (2 * scale) });
718
+
719
+ // If this is scale, then image area isn't allowed, so just delete it to be safe
720
+ delete displayArea.imageArea;
721
+ // Apply the pan values from the display area.
722
+ this.setDisplayAreaFit(displayArea);
723
+
724
+ // Need to ensure the focal point is aligned with the canvas size/position
725
+ // so that we don't get half pixel rendering, which causes additional
726
+ // moire patterns to be displayed.
727
+ // This is based on the canvas size having the center pixel be at a fractional
728
+ // position when the size is even, so matching a fractional position on the
729
+ // focal point to the center of an image pixel.
730
+ const { focalPoint, position, viewUp, viewPlaneNormal } = this.getCamera();
731
+ const focalChange = vec3.create();
732
+ if (canvas.height % 2) {
733
+ vec3.scaleAndAdd(focalChange, focalChange, viewUp, scale * 0.5 * spacing);
734
+ }
735
+ if (canvas.width % 2) {
736
+ const viewRight = vec3.cross(vec3.create(), viewUp, viewPlaneNormal);
737
+ vec3.scaleAndAdd(
738
+ focalChange,
739
+ focalChange,
740
+ viewRight,
741
+ scale * 0.5 * spacing
742
+ );
743
+ }
744
+ if (!focalChange[0] && !focalChange[1] && !focalChange[2]) {
745
+ return;
746
+ }
747
+ this.setCamera({
748
+ focalPoint: <Point3>vec3.add(vec3.create(), focalPoint, focalChange),
749
+ position: <Point3>vec3.add(vec3.create(), position, focalChange),
750
+ });
751
+ }
752
+
753
+ /**
754
+ * This applies a display area with a fit of the provided area to the
755
+ * available area.
756
+ * The zoom level is controlled by the imageArea parameter, which is a pair
757
+ * of percentage width in the horizontal and vertical dimension is scaled
758
+ * to fit the displayable area. Both values are taken into account, and the
759
+ * scaling is set so that both fractions of the image area are visible.
760
+ *
761
+ * The panning is controlled by the imageCanvasPoint, which has two
762
+ * values, teh imagePoint and the canvasPoint. They are fractional
763
+ * values of the image and canvas respectively, with the panning set to
764
+ * display the image pixel at the given fraction on top of the canvas at the
765
+ * given percentage. The default points are 0.5.
766
+ *
767
+ * For example, if the zoom level is [2,1], then the image is displayed
768
+ * such that at least twice the width is visible, and the height is visible.
769
+ * That will result in the image width being black, divided up on the left
770
+ * and right according to the imageCanvasPoint
771
+ *
772
+ * Then, if the imagePoint is [1,0] and the canvas point is [1,0], then
773
+ * the right most edge of the image, at the top of the image, will be
774
+ * displayed at the right most edge of the canvas, at the top.
775
+ *
776
+ */
777
+ protected setDisplayAreaFit(displayArea: DisplayArea) {
778
+ const { imageArea, imageCanvasPoint } = displayArea;
779
+
780
+ const devicePixelRatio = window?.devicePixelRatio || 1;
781
+ const imageData = this.getDefaultImageData();
782
+ if (!imageData) {
783
+ return;
784
+ }
785
+ const canvasWidth = this.sWidth / devicePixelRatio;
786
+ const canvasHeight = this.sHeight / devicePixelRatio;
787
+ const dimensions = imageData.getDimensions();
788
+ const canvasZero = this.worldToCanvas(imageData.indexToWorld([0, 0, 0]));
789
+ const canvasEdge = this.worldToCanvas(
790
+ imageData.indexToWorld([
791
+ dimensions[0] - 1,
792
+ dimensions[1] - 1,
793
+ dimensions[2],
794
+ ])
795
+ );
796
+
797
+ const canvasImage = [
798
+ Math.abs(canvasEdge[0] - canvasZero[0]),
799
+ Math.abs(canvasEdge[1] - canvasZero[1]),
800
+ ];
801
+ const [imgWidth, imgHeight] = canvasImage;
802
+
803
+ if (imageArea) {
804
+ const [areaX, areaY] = imageArea;
805
+ const requireX = Math.abs((areaX * imgWidth) / canvasWidth);
806
+ const requireY = Math.abs((areaY * imgHeight) / canvasHeight);
807
+
808
+ const initZoom = this.getZoom();
809
+ const fitZoom = this.getZoom(this.fitToCanvasCamera);
810
+ const absZoom = Math.min(1 / requireX, 1 / requireY);
811
+ const applyZoom = (absZoom * initZoom) / fitZoom;
812
+ this.setZoom(applyZoom, false);
813
+ }
814
+
815
+ // getting the image info
816
+ // getting the image info
817
+ if (imageCanvasPoint) {
818
+ const { imagePoint, canvasPoint = imagePoint || [0.5, 0.5] } =
819
+ imageCanvasPoint;
820
+ const [canvasX, canvasY] = canvasPoint;
821
+ const canvasPanX = canvasWidth * (canvasX - 0.5);
822
+ const canvasPanY = canvasHeight * (canvasY - 0.5);
823
+
824
+ const [imageX, imageY] = imagePoint || canvasPoint;
825
+ const useZoom = 1;
826
+ const imagePanX = useZoom * imgWidth * (0.5 - imageX);
827
+ const imagePanY = useZoom * imgHeight * (0.5 - imageY);
828
+
829
+ const newPositionX = imagePanX + canvasPanX;
830
+ const newPositionY = imagePanY + canvasPanY;
831
+
832
+ const deltaPoint2: Point2 = [newPositionX, newPositionY];
833
+ // Use getPan from current for the setting
834
+ vec2.add(deltaPoint2, deltaPoint2, this.getPan());
835
+ // The pan is part of the display area settings, not the initial camera, so
836
+ // don't store as initial camera here - that breaks rotation and other changes.
837
+ this.setPan(deltaPoint2, false);
676
838
  }
677
839
  }
678
840
 
@@ -715,7 +877,10 @@ class Viewport implements IViewport {
715
877
  const focalPoint = <Point3>[0, 0, 0];
716
878
  const imageData = this.getDefaultImageData();
717
879
 
718
- // Todo: remove this, this is just for tests passing
880
+ // The bounds are used to set the clipping view, which is then used to
881
+ // figure out the center point of each image. This needs to be the depth
882
+ // center, so the bounds need to be extended by the spacing such that the
883
+ // depth center is in the middle of each image.
719
884
  if (imageData) {
720
885
  const spc = imageData.getSpacing();
721
886
 
@@ -741,9 +906,14 @@ class Viewport implements IViewport {
741
906
 
742
907
  if (imageData) {
743
908
  const dimensions = imageData.getDimensions();
909
+ // TODO: This should be the line below, but that causes issues with existing
910
+ // tests. Not doing that adds significant fuzziness on rendering, so at
911
+ // some point it should be fixed.
912
+ // const middleIJK = dimensions.map((d) => Math.floor((d-1) / 2));
744
913
  const middleIJK = dimensions.map((d) => Math.floor(d / 2));
745
914
 
746
915
  const idx = [middleIJK[0], middleIJK[1], middleIJK[2]];
916
+ // Modifies the focal point in place, as this hits the vtk indexToWorld function
747
917
  imageData.indexToWorld(idx, focalPoint);
748
918
  }
749
919
 
@@ -755,37 +925,20 @@ class Viewport implements IViewport {
755
925
  const boundsAspectRatio = widthWorld / heightWorld;
756
926
  const canvasAspectRatio = canvasSize[0] / canvasSize[1];
757
927
 
758
- let radius;
928
+ const scaleFactor = boundsAspectRatio / canvasAspectRatio;
759
929
 
760
- if (boundsAspectRatio < canvasAspectRatio) {
761
- // can fit full height, so use it.
762
- radius = heightWorld / 2;
763
- } else {
764
- const scaleFactor = boundsAspectRatio / canvasAspectRatio;
765
-
766
- radius = (heightWorld * scaleFactor) / 2;
767
- }
768
-
769
- //const angle = vtkMath.radiansFromDegrees(activeCamera.getViewAngle())
770
- const parallelScale = this.insetImageMultiplier * radius;
771
-
772
- let w1 = bounds[1] - bounds[0];
773
- let w2 = bounds[3] - bounds[2];
774
- let w3 = bounds[5] - bounds[4];
775
- w1 *= w1;
776
- w2 *= w2;
777
- w3 *= w3;
778
- radius = w1 + w2 + w3;
930
+ const parallelScale =
931
+ scaleFactor < 1 // can fit full height, so use it.
932
+ ? (this.insetImageMultiplier * heightWorld) / 2
933
+ : (this.insetImageMultiplier * heightWorld * scaleFactor) / 2;
779
934
 
780
935
  // If we have just a single point, pick a radius of 1.0
781
- radius = radius === 0 ? 1.0 : radius;
782
-
783
936
  // compute the radius of the enclosing sphere
784
- radius = Math.sqrt(radius) * 0.5;
785
-
786
937
  // For 3D viewport, we should increase the radius to make sure the whole
787
938
  // volume is visible and we don't get clipping artifacts.
788
- radius = this.type === ViewportType.VOLUME_3D ? radius * 10 : radius;
939
+ const radius =
940
+ Viewport.boundsRadius(bounds) *
941
+ (this.type === ViewportType.VOLUME_3D ? 10 : 1);
789
942
 
790
943
  const distance = this.insetImageMultiplier * radius;
791
944
 
@@ -893,19 +1046,19 @@ class Viewport implements IViewport {
893
1046
  * computed from the current camera, where the initial pan
894
1047
  * value is [0,0].
895
1048
  */
896
- public getPan(): Point2 {
1049
+ public getPan(initialCamera = this.initialCamera): Point2 {
897
1050
  const activeCamera = this.getVtkActiveCamera();
898
1051
  const focalPoint = activeCamera.getFocalPoint() as Point3;
899
1052
 
900
1053
  const zero3 = this.canvasToWorld([0, 0]);
901
1054
  const initialCanvasFocal = this.worldToCanvas(
902
- <Point3>vec3.subtract(vec3.create(), this.initialCamera.focalPoint, zero3)
1055
+ <Point3>vec3.subtract([0, 0, 0], initialCamera.focalPoint, zero3)
903
1056
  );
904
1057
  const currentCanvasFocal = this.worldToCanvas(
905
- <Point3>vec3.subtract(vec3.create(), focalPoint, zero3)
1058
+ <Point3>vec3.subtract([0, 0, 0], focalPoint, zero3)
906
1059
  );
907
1060
  const result = <Point2>(
908
- vec2.subtract(vec2.create(), initialCanvasFocal, currentCanvasFocal)
1061
+ vec2.subtract([0, 0], initialCanvasFocal, currentCanvasFocal)
909
1062
  );
910
1063
  return result;
911
1064
  }
@@ -926,7 +1079,7 @@ class Viewport implements IViewport {
926
1079
  const previousCamera = this.getCamera();
927
1080
  const { focalPoint, position } = previousCamera;
928
1081
  const zero3 = this.canvasToWorld([0, 0]);
929
- const delta2 = vec2.subtract(vec2.create(), pan, this.getPan());
1082
+ const delta2 = vec2.subtract([0, 0], pan, this.getPan());
930
1083
  if (
931
1084
  Math.abs(delta2[0]) < 1 &&
932
1085
  Math.abs(delta2[1]) < 1 &&
@@ -956,9 +1109,9 @@ class Viewport implements IViewport {
956
1109
  * originally applied to the image. That is, on initial display,
957
1110
  * the zoom level is 1. Computed as a function of the camera.
958
1111
  */
959
- public getZoom(): number {
1112
+ public getZoom(compareCamera = this.initialCamera): number {
960
1113
  const activeCamera = this.getVtkActiveCamera();
961
- const { parallelScale: initialParallelScale } = this.initialCamera;
1114
+ const { parallelScale: initialParallelScale } = compareCamera;
962
1115
  return initialParallelScale / activeCamera.getParallelScale();
963
1116
  }
964
1117
 
@@ -1397,6 +1550,15 @@ class Viewport implements IViewport {
1397
1550
  return { widthWorld: maxX - minX, heightWorld: maxY - minY };
1398
1551
  }
1399
1552
 
1553
+ /**
1554
+ * Gets a view target specifying WHAT a view is displaying,
1555
+ * allowing for checking if a given image is displayed or could be displayed
1556
+ * in a given viewport.
1557
+ * See getViewPresentation for HOW a view is displayed.
1558
+ *
1559
+ * @param viewRefSpecifier - choose an alternate view to be specified, typically
1560
+ * a different slice index in the same set of images.
1561
+ */
1400
1562
  public getViewReference(
1401
1563
  viewRefSpecifier: ViewReferenceSpecifier = {}
1402
1564
  ): ViewReference {
@@ -1410,6 +1572,13 @@ class Viewport implements IViewport {
1410
1572
  return target;
1411
1573
  }
1412
1574
 
1575
+ /**
1576
+ * Find out if this viewport does or could show this view reference.
1577
+ *
1578
+ * @param options - allows specifying whether the view COULD display this with
1579
+ * some modification - either navigation or displaying as volume.
1580
+ * @returns true if the viewport could show this view reference
1581
+ */
1413
1582
  public isReferenceViewable(
1414
1583
  viewRef: ViewReference,
1415
1584
  options?: ReferenceCompatibleOptions
@@ -1437,6 +1606,83 @@ class Viewport implements IViewport {
1437
1606
  return true;
1438
1607
  }
1439
1608
 
1609
+ /**
1610
+ * Gets a view presentation information specifying HOW a viewport displays
1611
+ * something, but not what is being displayed.
1612
+ * See getViewReference to get information on WHAT is being displayed.
1613
+ *
1614
+ * This is intended to have information on how an image is presented to the user, without
1615
+ * specifying what image s displayed. All of this information is available
1616
+ * externally, but this method combines the parts of this that are appropriate
1617
+ * for remember or applying to other views, without necessarily needing to know
1618
+ * what all the atributes are. That differs from methods like getCamera which
1619
+ * fetch exact view details that are not likely to be identical between viewports
1620
+ * as they change sizes or apply to different images.
1621
+ *
1622
+ * Note that the results of this can be used on different viewports, for example,
1623
+ * the pan values can be applied to a volume viewport showing a CT, and a
1624
+ * stack viewport showing an ultrasound.
1625
+ *
1626
+ * The selector allows choosing which view presentation attributes to return.
1627
+ * Some default values are available from `Viewport.CameraViewPresentation` and
1628
+ * `Viewport.TransferViewPresentation`
1629
+ *
1630
+ * @param viewPresSel - select which attributes to display.
1631
+ */
1632
+ public getViewPresentation(
1633
+ viewPresSel: ViewPresentationSelector = {
1634
+ rotation: true,
1635
+ displayArea: true,
1636
+ zoom: true,
1637
+ pan: true,
1638
+ }
1639
+ ): ViewPresentation {
1640
+ const target: ViewPresentation = {};
1641
+
1642
+ const { rotation, displayArea, zoom, pan } = viewPresSel;
1643
+ if (rotation) {
1644
+ target.rotation = this.getRotation();
1645
+ }
1646
+ if (displayArea) {
1647
+ target.displayArea = this.getDisplayArea();
1648
+ }
1649
+ const initZoom = this.getZoom();
1650
+
1651
+ if (zoom) {
1652
+ target.zoom = initZoom;
1653
+ }
1654
+ if (pan) {
1655
+ target.pan = this.getPan();
1656
+ vec2.scale(target.pan, target.pan, 1 / initZoom);
1657
+ }
1658
+ return target;
1659
+ }
1660
+
1661
+ /**
1662
+ * Sets the given view. This can apply both the view reference and view presentation
1663
+ * without getting multiple event notifications on shared values like camera updates or
1664
+ * flickers as multiple changes are applied.
1665
+ *
1666
+ * @param viewRef - the basic positioning in terms of what image id/slice index/orientation to display
1667
+ * * The viewRef must be applicable to the current stack or volume, otherwise an exception will be thrown
1668
+ * @param viewPres - the presentation information to apply to the current image (as chosen above)
1669
+ */
1670
+ public setView(viewRef?: ViewReference, viewPres?: ViewPresentation) {
1671
+ if (viewPres) {
1672
+ const { displayArea, zoom = this.getZoom(), pan, rotation } = viewPres;
1673
+ if (displayArea !== this.getDisplayArea()) {
1674
+ this.setDisplayArea(displayArea);
1675
+ }
1676
+ this.setZoom(zoom);
1677
+ if (pan) {
1678
+ this.setPan(vec2.scale([0, 0], pan, zoom) as Point2);
1679
+ }
1680
+ if (rotation >= 0) {
1681
+ this.setRotation(rotation);
1682
+ }
1683
+ }
1684
+ }
1685
+
1440
1686
  protected _shouldUseNativeDataType() {
1441
1687
  const { useNorm16Texture, preferSizeOverAccuracy } =
1442
1688
  getConfiguration().rendering;
@@ -1556,6 +1802,22 @@ class Viewport implements IViewport {
1556
1802
  [p7, p8],
1557
1803
  ];
1558
1804
  }
1805
+
1806
+ /**
1807
+ * Computes the bounds radius value
1808
+ */
1809
+ static boundsRadius(bounds: number[]) {
1810
+ const w1 = (bounds[1] - bounds[0]) ** 2;
1811
+ const w2 = (bounds[3] - bounds[2]) ** 2;
1812
+ const w3 = (bounds[5] - bounds[4]) ** 2;
1813
+
1814
+ // If we have just a single point, pick a radius of 1.0
1815
+ // compute the radius of the enclosing sphere
1816
+ // For 3D viewport, we should increase the radius to make sure the whole
1817
+ // volume is visible and we don't get clipping artifacts.
1818
+ const radius = Math.sqrt(w1 + w2 + w3 || 1) * 0.5;
1819
+ return radius;
1820
+ }
1559
1821
  }
1560
1822
 
1561
1823
  export default Viewport;
@@ -1,5 +1,6 @@
1
1
  const VIEWPORT_ELEMENT = 'viewport-element';
2
2
  const CANVAS_CSS_CLASS = 'cornerstone-canvas';
3
+ export const EPSILON = 1e-4;
3
4
 
4
5
  /**
5
6
  * Create a canvas and append it to the element
@@ -13,6 +14,7 @@ function createCanvas(element: Element | HTMLDivElement): HTMLCanvasElement {
13
14
  canvas.style.position = 'absolute';
14
15
  canvas.style.width = '100%';
15
16
  canvas.style.height = '100%';
17
+ canvas.style.imageRendering = 'pixelated';
16
18
  canvas.classList.add(CANVAS_CSS_CLASS);
17
19
  element.appendChild(canvas);
18
20
 
@@ -30,6 +32,8 @@ export function createViewportElement(element: HTMLDivElement): HTMLDivElement {
30
32
  div.style.position = 'relative';
31
33
  div.style.width = '100%';
32
34
  div.style.height = '100%';
35
+ // Hide any canvas elements not viewable
36
+ div.style.overflow = 'hidden';
33
37
  div.classList.add(VIEWPORT_ELEMENT);
34
38
  element.appendChild(div);
35
39
 
@@ -39,6 +43,12 @@ export function createViewportElement(element: HTMLDivElement): HTMLDivElement {
39
43
  /**
40
44
  * Create a canvas or returns the one that already exists for a given element.
41
45
  * It first checks if the element has a canvas, if not it creates one and returns it.
46
+ * The canvas is updated for:
47
+ * 1. width/height in screen pixels to completely cover the div element
48
+ * 2. CSS width/height in CSS pixels to be the size of the physical screen pixels
49
+ * width and height (from #1)
50
+ * This allows drawing to the canvas and having pixel perfect/exact drawing to
51
+ * the physical screen pixels.
42
52
  *
43
53
  * @param element - An HTML Element
44
54
  * @returns canvas a Canvas DOM element
@@ -54,5 +64,26 @@ export default function getOrCreateCanvas(
54
64
  const internalDiv =
55
65
  element.querySelector(viewportElement) || createViewportElement(element);
56
66
 
57
- return internalDiv.querySelector(canvasSelector) || createCanvas(internalDiv);
67
+ const canvas = (internalDiv.querySelector(canvasSelector) ||
68
+ createCanvas(internalDiv)) as HTMLCanvasElement;
69
+ // Fit the canvas into the div
70
+ const rect = internalDiv.getBoundingClientRect();
71
+ const devicePixelRatio = window.devicePixelRatio || 1;
72
+
73
+ // The width/height is the number of physical pixels which will completely
74
+ // cover the div so that no pixels, fractional or full are left uncovered.
75
+ // Thus, it is the ceiling of the CSS size times the physical pixels.
76
+ // In theory, the physical pixels can be offset from CSS pixels, but in practice
77
+ // this hasn't been observed.
78
+ const width = Math.ceil(rect.width * devicePixelRatio);
79
+ const height = Math.ceil(rect.height * devicePixelRatio);
80
+ canvas.width = width;
81
+ canvas.height = height;
82
+ // Reset the size of the canvas to be the number of physical pixels,
83
+ // expressed as CSS pixels, with a tiny extra amount to prevent clipping
84
+ // to the next lower size in the physical display.
85
+ canvas.style.width = (width + EPSILON) / devicePixelRatio + 'px';
86
+ canvas.style.height = (height + EPSILON) / devicePixelRatio + 'px';
87
+
88
+ return canvas;
58
89
  }
@@ -918,7 +918,8 @@ class Cache implements ICache {
918
918
  const imageCacheOffsetMap = volume.imageCacheOffsetMap;
919
919
 
920
920
  if (imageCacheOffsetMap.size === 0) {
921
- console.warn('No cached images to restore for this volume.');
921
+ // This happens during testing and isn't an issue
922
+ // console.warn('No cached images to restore for this volume.');
922
923
  return;
923
924
  }
924
925