Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(resize): Optimize resizing process and maintain zoom level #3889

Merged
merged 17 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
Expand Down Expand Up @@ -204,6 +203,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
onElementDisabled(viewportInfo);
}

cornerstoneViewportService.disableElement(viewportId);

eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
Expand Down Expand Up @@ -348,8 +349,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
<React.Fragment>
<div className="viewport-wrapper">
<ReactResizeDetector
refreshMode="debounce"
refreshRate={50} // Wait 50 ms after last move to render
onResize={onResize}
targetRef={elementRef.current}
/>
Expand Down
10 changes: 10 additions & 0 deletions extensions/cornerstone/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ export default async function init({
}
);

// resize the cornerstone viewport service when the grid size changes
// IMPORTANT: this should happen outside of the OHIFCornerstoneViewport
// since it will trigger a rerender of each viewport and each resizing
// the offscreen canvas which would result in a performance hit, this should
// done only once per grid resize here. Doing it once here, allows us to reduce
// the refreshRage(in ms) to 10 from 50. I tried with even 1 or 5 ms it worked fine
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_SIZE_CHANGED, () => {
cornerstoneViewportService.resize(true);
});

initContextMenu({
cornerstoneViewportService,
customizationService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportsById: Map<string, ViewportInfo> = new Map();
viewportGridResizeObserver: ResizeObserver | null;
viewportsDisplaySets: Map<string, string[]> = new Map();
beforeResizeZoomLevels: Map<string, number> = new Map();

// Some configs
enableResizeDetector: true;
resizeRefreshRateMs: 200;
resizeRefreshMode: 'debounce';
servicesManager = null;

resizeQueue = [];
viewportResizeTimer = null;
gridResizeDelay = 50;
gridResizeTimeOut = null;

constructor(servicesManager: ServicesManager) {
super(EVENTS);
this.renderingEngine = null;
Expand Down Expand Up @@ -94,14 +100,84 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}

/**
* It triggers the resize on the rendering engine.
* It triggers the resize on the rendering engine, and renders the viewports
*
* @param isGridResize - if the resize is triggered by a grid resize
* this is used to avoid double resize of the viewports since if the
* grid is resized, all viewports will be resized so there is no need
* to resize them individually which will get triggered by their
* individual resize observers
*/
public resize() {
const immediate = true;
const keepCamera = true;
public resize(isGridResize = false) {
// if there is a grid resize happening, it means the viewport grid
// has been manipulated (e.g., panels closed, added, etc.) and we need
// to resize all viewports, so we will add a timeout here to make sure
// we don't double resize the viewports when viewports in the grid are
// resized individually
if (isGridResize) {
this.performResize();
this.resetGridResizeTimeout();
this.resizeQueue = [];
clearTimeout(this.viewportResizeTimer);
} else {
this.enqueueViewportResizeRequest();
}
}

private enqueueViewportResizeRequest() {
this.resizeQueue.push(false); // false indicates viewport resize

clearTimeout(this.viewportResizeTimer);
this.viewportResizeTimer = setTimeout(() => {
this.processViewportResizeQueue();
}, this.gridResizeDelay);
}

private processViewportResizeQueue() {
const isGridResizeInQueue = this.resizeQueue.some(isGridResize => isGridResize);
if (this.resizeQueue.length > 0 && !isGridResizeInQueue && !this.gridResizeTimeOut) {
this.performResize();
}

// Clear the queue after processing viewport resizes
this.resizeQueue = [];
}

private performResize() {
const isImmediate = false;

const viewports = this.getRenderingEngine().getViewports();

// Store the zoom scale before resizing.
viewports.forEach(viewport => {
const zoomLevel = viewport.getZoom();
sedghi marked this conversation as resolved.
Show resolved Hide resolved
this.beforeResizeZoomLevels.set(viewport.id, zoomLevel);
});

// Resize the rendering engine and render.
const renderingEngine = this.renderingEngine;
renderingEngine.resize(isImmediate);
renderingEngine.render();

// Reset the camera for viewports that should reset their camera on resize,
// which means only those viewports that have a zoom level of 1.
this.beforeResizeZoomLevels.forEach((zoomLevel, viewportId) => {
if (zoomLevel === 1) {
const viewport = renderingEngine.getViewport(viewportId);
viewport?.resetCamera();
}
});

// Resize and render the rendering engine again.
this.renderingEngine?.resize(isImmediate);
this.renderingEngine?.render();
}

this.renderingEngine.resize(immediate, keepCamera);
this.renderingEngine.render();
private resetGridResizeTimeout() {
clearTimeout(this.gridResizeTimeOut);
this.gridResizeTimeOut = setTimeout(() => {
this.gridResizeTimeOut = null;
}, this.gridResizeDelay);
}

/**
Expand Down Expand Up @@ -740,8 +816,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { dimensions } = imageVolume;
const slabThickness = Math.sqrt(
dimensions[0] * dimensions[0] +
dimensions[1] * dimensions[1] +
dimensions[2] * dimensions[2]
dimensions[1] * dimensions[1] +
dimensions[2] * dimensions[2]
);

return slabThickness;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface IViewportService {
* the element for resizing events
* @param {*} elementRef
*/
resize(element: HTMLDivElement): void;
resize(isGridResize: boolean): void;
/**
* Removes the viewport from cornerstone, and destroys the rendering engine
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const CornerstoneViewportDownloadForm = ({
renderingEngine.resize();

// Trigger the render on the viewport to update the on screen
downloadViewport.resetCamera();
downloadViewport.render();

downloadViewportElement.addEventListener(
Expand Down Expand Up @@ -223,7 +224,6 @@ const CornerstoneViewportDownloadForm = ({
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
defaultSize={DEFAULT_SIZE}
canvasClass={'cornerstone-canvas'}
activeViewportElement={activeViewportElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
Expand Down
2 changes: 1 addition & 1 deletion platform/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"react-dom": "^17.0.2",
"react-dropzone": "^10.1.7",
"react-i18next": "^12.2.2",
"react-resize-detector": "^6.7.6",
"react-resize-detector": "^9.1.0",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
},
Expand Down
29 changes: 22 additions & 7 deletions platform/app/src/components/ViewportGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useCallback } from 'react';
import React, { useEffect, useCallback, useRef } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import PropTypes from 'prop-types';
import { ServicesManager, Types, MeasurementService } from '@ohif/core';
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
Expand All @@ -13,6 +14,7 @@ function ViewerViewportGrid(props) {

const { layout, activeViewportId, viewports } = viewportGrid;
const { numCols, numRows } = layout;
const elementRef = useRef(null);

// TODO -> Need some way of selecting which displaySets hit the viewports.
const { displaySetService, measurementService, hangingProtocolService, uiNotificationService } = (
Expand Down Expand Up @@ -342,13 +344,26 @@ function ViewerViewportGrid(props) {
}

return (
<ViewportGrid
numRows={numRows}
numCols={numCols}
<div
ref={elementRef}
className="h-full w-full"
>
{/* {ViewportPanes} */}
{getViewportPanes()}
</ViewportGrid>
<ViewportGrid
numRows={numRows}
numCols={numCols}
>
<ReactResizeDetector
refreshMode="debounce"
sedghi marked this conversation as resolved.
Show resolved Hide resolved
refreshRate={7} // ms seems to be fine for 10 viewports
onResize={() => {
viewportGridService.setViewportGridSizeChanged();
}}
targetRef={elementRef.current}
/>
{/* {ViewportPanes} */}
{getViewportPanes()}
</ViewportGrid>
</div>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class ViewportGridService extends PubSubService {
ACTIVE_VIEWPORT_ID_CHANGED: 'event::activeviewportidchanged',
LAYOUT_CHANGED: 'event::layoutChanged',
GRID_STATE_CHANGED: 'event::gridStateChanged',
GRID_SIZE_CHANGED: 'event::gridSizeChanged',
};

public static REGISTRATION = {
Expand Down Expand Up @@ -64,7 +65,6 @@ class ViewportGridService extends PubSubService {

public setActiveViewportId(id: string) {
this.serviceImplementation._setActiveViewport(id);
const state = this.getState();
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
viewportId: id,
});
Expand All @@ -79,6 +79,13 @@ class ViewportGridService extends PubSubService {
return state.activeViewportId;
}

public setViewportGridSizeChanged() {
const state = this.getState();
this._broadcastEvent(this.EVENTS.GRID_SIZE_CHANGED, {
state,
});
}

public setDisplaySetsForViewport(props) {
// Just update a single viewport, but use the multi-viewport update for it.
this.setDisplaySetsForViewports([props]);
Expand Down
1 change: 1 addition & 0 deletions platform/ui/src/contextProviders/ViewportGridProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ export function ViewportGridProvider({ children, service }) {
set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events
getNumViewportPanes,
getActiveViewportOptionByKey,
setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props),
};

return (
Expand Down
Loading
Loading