Skip to content

Commit

Permalink
feat: overlay component (#21)
Browse files Browse the repository at this point in the history
* fix default displayset options

* add viewport overlay

* apply review comments
  • Loading branch information
sedghi committed May 16, 2022
1 parent e943f60 commit 5fd4d45
Show file tree
Hide file tree
Showing 18 changed files with 392 additions and 114 deletions.
208 changes: 208 additions & 0 deletions extensions/cornerstone-3d/src/Viewport/CornerstoneOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';

import Cornerstone3DViewportService from '../services/ViewportService/Cornerstone3DViewportService';

function CornerstoneOverlay({
viewportData,
imageIndex,
viewportIndex,
ToolBarService,
}) {
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);

const getCornerstoneViewport = useCallback(
viewportIndex => {
const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex(
viewportIndex
);

if (!viewportInfo) {
return;
}

const viewportId = viewportInfo.getViewportId();
const viewport = Cornerstone3DViewportService.getCornerstone3DViewport(
viewportId
);

return viewport;
},
[viewportIndex, viewportData]
);

/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(ToolBarService.getActiveTools());
}, []);

/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const viewport = getCornerstoneViewport(viewportIndex);

if (!viewport) {
return;
}

const { element } = viewport;

const updateVOI = eventDetail => {
const { range } = eventDetail.detail;

if (!range) {
return;
}

const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
);

setVOI({ windowCenter, windowWidth });
};

element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);

return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportIndex, viewportData]);

/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const viewport = getCornerstoneViewport(viewportIndex);
if (!viewport) {
return;
}

const { element } = viewport;

const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;

if (previousCamera.parallelScale !== camera.parallelScale) {
const viewport = getCornerstoneViewport(viewportIndex);

const { dimensions, spacing } = viewport.getImageData();

// Todo: handle for the volume viewports with directions
const scale = (dimensions[0] * spacing[0]) / camera.parallelScale;
setScale(scale);
}
};

element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);

return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData]);

/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = ToolBarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(ToolBarService.getActiveTools());
}
);

return () => {
unsubscribe();
};
}, [ToolBarService]);

const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;

if (activeTools.includes('WindowLevel')) {
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}

return (
<div className="flex flex-row">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}

if (activeTools.includes('Zoom')) {
return (
<div className="flex flex-row">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}

return null;
}, [voi, scale, activeTools]);

const getTopRightContent = useCallback(() => {
const { stack } = viewportData;
const imageId = stack.imageIds[imageIndex];

const generalImageModule =
metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;

const stackSize = stack.imageIds ? stack.imageIds.length : 0;

return (
<div className="flex flex-row">
<span className="mr-1">I:</span>
<span className="font-light">
{instanceNumber !== undefined
? `${instanceNumber} (${imageIndex}/${stackSize})`
: `${imageIndex}/${stackSize}`}
</span>
</div>
);
}, [imageIndex, viewportData]);

if (!viewportData) {
return null;
}

// Todo: fix this for volume later
const { stack } = viewportData;

if (!stack || stack.imageIds.length === 0) {
throw new Error(
'ViewportOverlay: only viewports with imageIds is supported at this time'
);
}

return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
/>
);
}

CornerstoneOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportIndex: PropTypes.number,
};

export default CornerstoneOverlay;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { utilities } from '@cornerstonejs/tools';
import { Enums } from '@cornerstonejs/core';

import Cornerstone3DViewportService from '../services/ViewportService/Cornerstone3DViewportService';
import CornerstoneOverlay from './CornerstoneOverlay';

import './OHIFCornerstone3DViewport.css';

Expand Down Expand Up @@ -57,12 +58,17 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = props;

const [viewportData, setViewportData] = useState(null);
const [scrollbarIndex, setScrollbarIndex] = useState(0);
const [imageIndex, setImageIndex] = useState(0);
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [_, viewportGridService] = useViewportGrid();

const elementRef = useRef();
const { MeasurementService, DisplaySetService } = servicesManager.services;

const {
MeasurementService,
DisplaySetService,
ToolBarService,
} = servicesManager.services;

// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
Expand Down Expand Up @@ -122,7 +128,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
const index = viewportData.stack?.imageIds.indexOf(imageId);

if (index !== -1) {
setScrollbarIndex(index);
setImageIndex(index);
}
};

Expand Down Expand Up @@ -188,7 +194,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewport.setImageIdIndex(imageIndex).then(() => {
// Update scrollbar index
const currentIndex = viewport.getCurrentImageIdIndex();
setScrollbarIndex(currentIndex);
setImageIndex(currentIndex);
});
},
[viewportIndex, viewportData]
Expand Down Expand Up @@ -216,7 +222,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
onChange={evt => onImageScrollbarChange(evt, viewportIndex)}
max={viewportData ? viewportData.stack?.imageIds?.length - 1 : 0}
height={scrollbarHeight}
value={scrollbarIndex}
value={imageIndex}
/>
<CornerstoneOverlay
viewportData={viewportData}
imageIndex={imageIndex}
viewportIndex={viewportIndex}
ToolBarService={ToolBarService}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Cornerstone3DViewportService implements IViewportService {
viewportIndex: number,
viewportData: unknown,
viewportOptions: ViewportOptions,
displaySetOptions: unknown[]
displaySetOptions: DisplaySetOptions[]
): void {
const renderingEngine = this.getRenderingEngine();
const viewportInfo = this.viewportsInfo.get(viewportIndex);
Expand All @@ -153,7 +153,7 @@ class Cornerstone3DViewportService implements IViewportService {
let displaySetOptionsToUse = currentDisplaySetOptions;
if (displaySetOptions?.length) {
displaySetOptionsToUse = [
...(currentDisplaySetOptions ?? []),
...currentDisplaySetOptions,
...displaySetOptions,
];
}
Expand All @@ -178,12 +178,7 @@ class Cornerstone3DViewportService implements IViewportService {
};

renderingEngine.enableElement(viewportInput);
this._setDisplaySets(
viewportId,
viewportData,
viewportOptionsToUse,
displaySetOptionsToUse
);
this._setDisplaySets(viewportId, viewportData, viewportInfo);
}

public getCornerstone3DViewport(viewportId: string): StackViewport | null {
Expand Down Expand Up @@ -217,16 +212,17 @@ class Cornerstone3DViewportService implements IViewportService {
return null;
}

_setStackViewport(viewport, viewportData, displaySetOptions) {
_setStackViewport(viewport, viewportData, viewportInfo) {
const displaySetOptions = viewportInfo.getDisplaySetOptions();

const { imageIds, initialImageIdIndex } = viewportData.stack;
// Todo: handle fusion stack when it is implemented
const { voi, voiInverted } = displaySetOptions[0];

const properties = {};
if (Array.isArray(voi)) {
if (voi.windowWidth || voi.windowCenter) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
voi[0],
voi[1]
voi.windowWidth,
voi.windowCenter
);
properties.voiRange = { lower, upper };
}
Expand All @@ -244,13 +240,12 @@ class Cornerstone3DViewportService implements IViewportService {
_setDisplaySets(
viewportId: string,
viewportData: unknown,
viewportOptions: ViewportOptions,
displaySetOptions: DisplaySetOptions
viewportInfo: ViewportInfo
): void {
const viewport = this.renderingEngine.getViewport(viewportId);
const viewport = this.getCornerstone3DViewport(viewportId);

if (viewport instanceof StackViewport) {
this._setStackViewport(viewport, viewportData, displaySetOptions);
this._setStackViewport(viewport, viewportData, viewportInfo);
} else {
throw new Error('Unsupported viewport type');
}
Expand Down
Loading

0 comments on commit 5fd4d45

Please sign in to comment.