Skip to content

Commit 650ba56

Browse files
committed
canvas packing
1 parent a851443 commit 650ba56

3 files changed

Lines changed: 318 additions & 33 deletions

File tree

packages/core/src/RenderingEngine/RenderingEngine.ts

Lines changed: 136 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import StackViewport from './StackViewport';
1111
import viewportTypeUsesCustomRenderingPipeline from './helpers/viewportTypeUsesCustomRenderingPipeline';
1212
import getOrCreateCanvas from './helpers/getOrCreateCanvas';
1313
import { getShouldUseCPURendering, isCornerstoneInitialized } from '../init';
14+
import { calculateViewportOffsets } from './helpers/rectanglePacking';
1415
import type IStackViewport from '../types/IStackViewport';
1516
import type IVolumeViewport from '../types/IVolumeViewport';
1617
import viewportTypeToViewportClass from './helpers/viewportTypeToViewportClass';
@@ -40,6 +41,10 @@ interface ViewportDisplayCoords {
4041
// Rendering engines seem to not like rendering things less than 2 pixels per side
4142
const VIEWPORT_MIN_SIZE = 2;
4243

44+
// Maximum size of the offscreen canvas
45+
const MAX_WIDTH = 16384;
46+
const MAX_HEIGHT = 16384;
47+
4348
/**
4449
* A RenderingEngine takes care of the full pipeline of creating viewports and rendering
4550
* them on a large offscreen canvas and transmitting this data back to the screen. This allows us
@@ -681,19 +686,43 @@ class RenderingEngine {
681686

682687
// 2.d Re-position previous viewports on the offScreen Canvas based on the new
683688
// offScreen canvas size
684-
const xOffset = this._resize(
689+
this._resize(
685690
viewportsDrivenByVtkJs as (IStackViewport | IVolumeViewport)[],
686691
offScreenCanvasWidth,
687692
offScreenCanvasHeight
688693
);
689694

690695
const internalViewportEntry = { ...viewportInputEntry, canvas };
691696

697+
// Calculate the position for the new viewport using packing
698+
const allCanvases = [...canvasesDrivenByVtkJs];
699+
const viewportInputs = allCanvases.map((c, index) => ({
700+
id:
701+
index === allCanvases.length - 1
702+
? viewportInputEntry.viewportId
703+
: `viewport-${index}`,
704+
canvas: {
705+
width: c.width,
706+
height: c.height,
707+
},
708+
}));
709+
710+
const packedOffsets = calculateViewportOffsets(
711+
viewportInputs,
712+
MAX_WIDTH,
713+
MAX_HEIGHT
714+
);
715+
716+
const newViewportOffset = packedOffsets.find(
717+
(offset) => offset.id === viewportInputEntry.viewportId
718+
) || { xOffset: 0, yOffset: 0 };
719+
692720
// 3 Add the requested viewport to rendering Engine
693721
this.addVtkjsDrivenViewport(internalViewportEntry, {
694722
offScreenCanvasWidth,
695723
offScreenCanvasHeight,
696-
xOffset,
724+
xOffset: newViewportOffset.xOffset,
725+
yOffset: newViewportOffset.yOffset,
697726
});
698727
}
699728

@@ -734,6 +763,7 @@ class RenderingEngine {
734763
offScreenCanvasWidth: number;
735764
offScreenCanvasHeight: number;
736765
xOffset: number;
766+
yOffset?: number;
737767
}
738768
): void {
739769
const { element, canvas, viewportId, type, defaultOptions } =
@@ -742,8 +772,12 @@ class RenderingEngine {
742772
// Make the element not focusable, we use this for modifier keys to work
743773
element.tabIndex = -1;
744774

745-
const { offScreenCanvasWidth, offScreenCanvasHeight, xOffset } =
746-
offscreenCanvasProperties;
775+
const {
776+
offScreenCanvasWidth,
777+
offScreenCanvasHeight,
778+
xOffset,
779+
yOffset = 0,
780+
} = offscreenCanvasProperties;
747781

748782
// 1. Calculate the size of location of the viewport on the offScreen canvas
749783
const {
@@ -759,7 +793,8 @@ class RenderingEngine {
759793
viewportInputEntry,
760794
offScreenCanvasWidth,
761795
offScreenCanvasHeight,
762-
xOffset
796+
xOffset,
797+
yOffset
763798
);
764799
// 2. Add a renderer to the offScreenMultiRenderWindow
765800
this.offscreenMultiRenderWindow.addRenderer({
@@ -925,8 +960,30 @@ class RenderingEngine {
925960
}*/
926961

927962
// 3. Adding the viewports based on the viewportInputEntry definition to the
928-
// rendering engine.
929-
let xOffset = 0;
963+
// rendering engine using rectangle packing
964+
const viewportInputs = vtkDrivenCanvases.map((canvas, index) => ({
965+
id: viewportInputEntries[index].viewportId,
966+
canvas: {
967+
width: canvas.width,
968+
height: canvas.height,
969+
},
970+
}));
971+
972+
const packedOffsets = calculateViewportOffsets(
973+
viewportInputs,
974+
MAX_WIDTH,
975+
MAX_HEIGHT
976+
);
977+
978+
// Create a map for quick lookup
979+
const offsetMap = new Map<string, { xOffset: number; yOffset: number }>();
980+
packedOffsets.forEach((offset) => {
981+
offsetMap.set(offset.id, {
982+
xOffset: offset.xOffset,
983+
yOffset: offset.yOffset,
984+
});
985+
});
986+
930987
for (let i = 0; i < viewportInputEntries.length; i++) {
931988
const vtkDrivenViewportInputEntry = viewportInputEntries[i];
932989
const canvas = vtkDrivenCanvases[i];
@@ -935,15 +992,19 @@ class RenderingEngine {
935992
canvas,
936993
};
937994

995+
const offsets = offsetMap.get(
996+
vtkDrivenViewportInputEntry.viewportId
997+
) || {
998+
xOffset: 0,
999+
yOffset: 0,
1000+
};
1001+
9381002
this.addVtkjsDrivenViewport(internalViewportEntry, {
9391003
offScreenCanvasWidth,
9401004
offScreenCanvasHeight,
941-
xOffset,
1005+
xOffset: offsets.xOffset,
1006+
yOffset: offsets.yOffset,
9421007
});
943-
944-
// Incrementing the xOffset which provides the horizontal location of each
945-
// viewport on the offScreen canvas
946-
xOffset += canvas.width;
9471008
}
9481009
}
9491010
}
@@ -959,19 +1020,40 @@ class RenderingEngine {
9591020
} {
9601021
const { offScreenCanvasContainer, offscreenMultiRenderWindow } = this;
9611022

962-
// 1. Calculated the height of the offScreen canvas to be the maximum height
963-
// between canvases
964-
const offScreenCanvasHeight = Math.max(
965-
...canvasesDrivenByVtkJs.map((canvas) => canvas.height)
1023+
// Use rectangle packing to calculate optimal canvas dimensions
1024+
const viewportInputs = canvasesDrivenByVtkJs.map((canvas, index) => ({
1025+
id: `viewport-${index}`,
1026+
canvas: {
1027+
width: canvas.width,
1028+
height: canvas.height,
1029+
},
1030+
}));
1031+
1032+
const packedOffsets = calculateViewportOffsets(
1033+
viewportInputs,
1034+
MAX_WIDTH,
1035+
MAX_HEIGHT
9661036
);
9671037

968-
// 2. Calculating the width of the offScreen canvas to be the sum of all
1038+
// Calculate total dimensions from packed rectangles
9691039
let offScreenCanvasWidth = 0;
1040+
let offScreenCanvasHeight = 0;
9701041

971-
canvasesDrivenByVtkJs.forEach((canvas) => {
972-
offScreenCanvasWidth += canvas.width;
1042+
packedOffsets.forEach((offset) => {
1043+
offScreenCanvasWidth = Math.max(
1044+
offScreenCanvasWidth,
1045+
offset.xOffset + offset.width
1046+
);
1047+
offScreenCanvasHeight = Math.max(
1048+
offScreenCanvasHeight,
1049+
offset.yOffset + offset.height
1050+
);
9731051
});
9741052

1053+
// Ensure minimum dimensions
1054+
offScreenCanvasWidth = Math.max(offScreenCanvasWidth, 1);
1055+
offScreenCanvasHeight = Math.max(offScreenCanvasHeight, 1);
1056+
9751057
// @ts-expect-error
9761058
offScreenCanvasContainer.width = offScreenCanvasWidth;
9771059
// @ts-expect-error
@@ -996,12 +1078,36 @@ class RenderingEngine {
9961078
viewportsDrivenByVtkJs: (IStackViewport | IVolumeViewport)[],
9971079
offScreenCanvasWidth: number,
9981080
offScreenCanvasHeight: number
999-
): number {
1000-
// Redefine viewport properties
1001-
let _xOffset = 0;
1081+
): void {
1082+
// Use rectangle packing to calculate viewport positions
1083+
const viewportInputs = viewportsDrivenByVtkJs.map((viewport) => ({
1084+
id: viewport.id,
1085+
canvas: {
1086+
width: viewport.canvas.width,
1087+
height: viewport.canvas.height,
1088+
},
1089+
}));
1090+
1091+
const packedOffsets = calculateViewportOffsets(
1092+
viewportInputs,
1093+
MAX_WIDTH,
1094+
MAX_HEIGHT
1095+
);
1096+
1097+
// Create a map for quick lookup
1098+
const offsetMap = new Map<string, { xOffset: number; yOffset: number }>();
1099+
packedOffsets.forEach((offset) => {
1100+
offsetMap.set(offset.id, {
1101+
xOffset: offset.xOffset,
1102+
yOffset: offset.yOffset,
1103+
});
1104+
});
10021105

1106+
// Update viewport positions using packed offsets
10031107
for (let i = 0; i < viewportsDrivenByVtkJs.length; i++) {
10041108
const viewport = viewportsDrivenByVtkJs[i];
1109+
const offsets = offsetMap.get(viewport.id) || { xOffset: 0, yOffset: 0 };
1110+
10051111
const {
10061112
sxStartDisplayCoords,
10071113
syStartDisplayCoords,
@@ -1015,11 +1121,10 @@ class RenderingEngine {
10151121
viewport as IViewport,
10161122
offScreenCanvasWidth,
10171123
offScreenCanvasHeight,
1018-
_xOffset
1124+
offsets.xOffset,
1125+
offsets.yOffset
10191126
);
10201127

1021-
_xOffset += viewport.canvas.width;
1022-
10231128
viewport.sx = sx;
10241129
viewport.sy = sy;
10251130
viewport.sWidth = sWidth;
@@ -1034,9 +1139,6 @@ class RenderingEngine {
10341139
syEndDisplayCoords,
10351140
]);
10361141
}
1037-
1038-
// Returns the final xOffset
1039-
return _xOffset;
10401142
}
10411143

10421144
/**
@@ -1051,20 +1153,21 @@ class RenderingEngine {
10511153
viewport: InternalViewportInput | IViewport,
10521154
offScreenCanvasWidth: number,
10531155
offScreenCanvasHeight: number,
1054-
_xOffset: number
1156+
_xOffset: number,
1157+
_yOffset: number = 0
10551158
): ViewportDisplayCoords {
10561159
const { canvas } = viewport;
10571160
const { width: sWidth, height: sHeight } = canvas;
10581161

10591162
// Update the canvas drawImage offsets.
10601163
const sx = _xOffset;
1061-
const sy = 0;
1164+
const sy = _yOffset;
10621165

10631166
const sxStartDisplayCoords = sx / offScreenCanvasWidth;
10641167

1065-
// Need to offset y if it not max height
1066-
const syStartDisplayCoords =
1067-
sy + (offScreenCanvasHeight - sHeight) / offScreenCanvasHeight;
1168+
// VTK uses bottom-left origin, while canvas uses top-left
1169+
// We need to invert the Y coordinate for VTK
1170+
const syStartDisplayCoords = 1 - (sy + sHeight) / offScreenCanvasHeight;
10681171

10691172
const sWidthDisplayCoords = sWidth / offScreenCanvasWidth;
10701173
const sHeightDisplayCoords = sHeight / offScreenCanvasHeight;

packages/core/src/RenderingEngine/helpers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import addVolumesToViewports from './addVolumesToViewports';
66
import volumeNewImageEventDispatcher from './volumeNewImageEventDispatcher';
77
import addImageSlicesToViewports from './addImageSlicesToViewports';
88

9+
export * from './rectanglePacking';
10+
911
export {
1012
createVolumeActor,
1113
createVolumeMapper,

0 commit comments

Comments
 (0)