feat: cornerstone3D stack and volume viewports (#2787)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: cs3d tools and toolGroups (#20)

* add more tools to work with cs3d mode

* fix hotkeys

* add stack manager usage for stach viewports

* add image scrollbar

* wip viewport overlay

* fix toAnnotation schema for tools

* fix the unnecessary size change that triggered resize

* hanging protocol improvement to allow unmatched errors

* study description matching for hanging protocol

* fix the displaysetOptions to work

* fix handle the active tool when a new viewport is added

* fix separate toolGroups for mode

* apply review comments

* apply review comments

* yarn lock

* feat: overlay component (#21)

* fix default displayset options

* add viewport overlay

* apply review comments

* feat: loading and orientation indicators (#22)

* add loading indicator

* add orientation marker initial work

* apply review comments

* fix: orientation markers (#23)

* finished the orientation markers

* fix various broken cypress tests

* apply review comments

* update yarn lock

* feat: re-working measurement tracking mode with cornerstone3d (#2805)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: Add Measurement tracking mode with cs3D (#2789)

* feat: first render for cornerstone3d tracked viewport

* make tool active work

* wip for SR extension

* renamed dicom sr to cornerstone dicom sr

* remove cornerstone from dicom pdf and video

* move dicom sr logic to sr extension

* feat: Add hydration for the length tool

* fix SR display tool for length using cs3d

* fix default config

* fix: various bugs with sr viewport and tracking

* fix promptying to continue tracking for when SR is created

* feat: add keep trackign of unique identifiers

* fix hydration for same imageIds

* feat: Add SR toolGroup creation on modeEnter

* feat: remove the need for separate mapper for SR hydration

* add SR display for ellipse

* handle hydration of elliptical ROI tool

* remove cornerstone extension

* add arrow mapping

* feat: Add ArrowAnnotate SR display and hydration

* apply review comments

* apply review comments

* move viewport labels to the viewportData

* apply review comments

* fix: integration cypress tests with cornerstone3D and add CINE tool (#2795)

* fix: integration cypress tests with cornerstone3D

* revert to addOrUpdate as it makes more sense

* fix local drag and drop

* fix tests

* move dicomLoaderService to cornerstone extension

* fix various import bugs

* fix bug for local PT series

* fix various unit tests

* bump cs3d versions

* add angle and magnify tool

* bump dependencies to avoid broken peerDeps

* feat: add initial work for capture using cs3d

* feat: show annotations on the image capture

* feat: add svg layer export

* feat: Add CINE Tool

* feat: remove unnecessary viewport rendering for cine state changes

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>

* docs: modify and improve documentation (#2800)

* cleanup docs versionings

* feat: Add all version explanations

* version docs for 3.0

* wip for changing docs

* wip for updated docs

* add utility module documentation

* fix demo with nohoisting of history

* add slides and video to resources

* apply review comments

* fix: drag and drop SR into SR viewport (#2803)

* update yarn lock

Co-authored-by: Erik <erik.sweed@gmail.com>

* update pathnames to match v3-stable

* update the e2e pathname

* fix: various bugs with regard to tracking workflow (#2811)

* fix: various issues with measurement panel

* fix: update default tool style for annotations

* fix: annotatoin label getting removed

* feat: Add backward compatibility for SR hydration with legacy cornerstone

* fix: cursors and ellipse ROI max style

* fix: ArrowAnnotate SRDisplay

* apply review comments

* bump package versions

* fix: bugin rehydration of SR

* fix: e2e tests

* fix active viewport thickness and arrowTool ui

* add readme for measurement tracking

* use uploaded image for readme

* add back images

* try to fix e2e test

* fix: window level presets hotkeys

* Update README.md

* update yarn lock

* feat: volume api and TMTV mode (#2817)

* feat: volumeAPI and TMTV mode

* feat: use cs3d cache service to obtain viewportData

* wip for volume api

* wip: fusion viewport

* feat: add blend mode option

* wip for image scrollbar

* fix drag and drop thumbnail

* wip for image scrollbar for voluems

* fix: element mismatch bug for scroll

* feat: Add image scrollbar to volumes

* feat: add syncGroups to volume api

* feat: add tmtv mode initial setup

* feat: add initial image options for the stack viewports

* feat: Add custom load strategy for volume viewports via HP

* apply review comments

fix: Jump presets cs3d (#2812)

* feat: Add JumpPreset to OHIF for Cornerstone3D

* fix: accessing viewport service from servicesManager

feat: volume API and TMTV mode (#2814)

* fix: do not display overlays on mip viewports

* feat: add optional disableCommands for toggle buttons

* fix: toggleCrossharis

* feat: config the crosshairs

* feat: add PetSUV Panel for changing metadata

* feat: initial work for the rectangleROIThreshold panel

* wip: measurement service

* roi threshold working

* feat: Add displayText to segmentations

* feat: add remove segmentation

* feat: add csv export

* feat: add RT export for annotations in tmtv mode

* fix: fusion to use pt in tmtv mode and measuremet mappings

* fix: various bugs

* apply review comments

* apply review comments

* feat: add fusion color maps

* add readme to tmtv mode

* Update README.md

* fix: try to fix build

* fix unit tests

* Update README.md

* feat: add about to the panel

* fix: changing strategy in roi panel

* apply review comments

* update package versions

* wip for stackPrefetch

* fix: cornerstone3d hydration and renaming (#2818)

* update readme

* renamed cornerstone extension

* wip for renaming cornerstone3D variables

* wip for renaming cornerstone3D variables

* wip for fixing bugs for SR viewport

* fix: jumpToMeasurement and initial label after hydration

* fix: fileName capitalization

* feat: use the new prefetch stack in the cs3d (#2820)

* feat: use the new prefetch stack in the cs3d

* use viewport scroll api for stack viewport

* fix cine stop when scrollbar changes

* feat: use new prefetch events

* fix loading state to not show repeatedly

* fix: various bugs for tmtv mode thresholding and new icons (#2823)

* feat: make tmtv mode available in worklist

* feat: add new icons for tmtv mode

* feat: add fusion color icon

* fix: parallel scale calculation

* fix: bump Cornerstone to get large image support working

* fix: Fix issues with CPU viewport flipping, including config files

* fix: bump cornerstone to fix magnify tool

* fix: Bump Cornerstone version to fix resetCamera issue in StackViewport

* ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys

* fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors

* feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838)

Also works with the compile time add that the existing cli uses, so that both build types work.
Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes.

* bump: dependency versions to fix hydration bugs (#2848)

* bump: dcmjs version to fix hydration bugs

* try to fix tests

* bump dependency versions

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
authored and GitHub committed 2022-07-27 12:39:04 -04:00
1 parent 86dcaf6293
commit bd1c38c00c
668 files changed
+41669 -13410

No files matched your search

@@ -166,9 +166,7 @@ Button.propTypes = {
size: PropTypes.oneOf(['small', 'medium', 'large', 'initial', 'inherit']),
/** Button corner roundness */
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
/** Button variants */
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
/** Button color */
variant: PropTypes.oneOf(['text', 'outlined', 'contained', 'disabled']),
color: PropTypes.oneOf([
'default',
'primary',
@@ -1,3 +1,3 @@
import HotkeyField from './HotkeyField.jsx';
import HotkeyField from './HotkeyField.tsx';
export default HotkeyField;
@@ -1,3 +1,3 @@
import HotkeysPreferences from './HotkeysPreferences.jsx';
import HotkeysPreferences from './HotkeysPreferences.tsx';
export default HotkeysPreferences;
@@ -55,10 +55,14 @@ import toolFlipHorizontal from './../../assets/icons/tool-flip-horizontal.svg';
import toolInvert from './../../assets/icons/tool-invert.svg';
import toolRotateRight from './../../assets/icons/tool-rotate-right.svg';
import toolCine from './../../assets/icons/tool-cine.svg';
import toolCrosshair from './../../assets/icons/tool-crosshair.svg';
import toolProbe from './../../assets/icons/tool-probe.svg';
import toolAngle from './../../assets/icons/tool-angle.svg';
import toolReset from './../../assets/icons/tool-reset.svg';
import toolRectangle from './../../assets/icons/tool-rectangle.svg';
import toolFusionColor from './../../assets/icons/tool-fusion-color.svg';
import toolCreateThreshold from './../../assets/icons/tool-create-threshold.svg';
import editPatient from './../../assets/icons/edit-patient.svg';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
@@ -120,10 +124,14 @@ const ICONS = {
'tool-invert': toolInvert,
'tool-rotate-right': toolRotateRight,
'tool-cine': toolCine,
'tool-crosshair': toolCrosshair,
'tool-probe': toolProbe,
'tool-angle': toolAngle,
'tool-reset': toolReset,
'tool-rectangle': toolRectangle,
'tool-fusion-color': toolFusionColor,
'tool-create-threshold': toolCreateThreshold,
'edit-patient': editPatient,
/** Old OHIF */
'old-trash': oldTrash,
@@ -0,0 +1,106 @@
.scroll {
height: 100%;
padding: 5px;
position: absolute;
right: 0;
top: 0;
}
.scroll .scroll-holder {
height: calc(100% - 20px);
margin-top: 5px;
position: relative;
width: 12px;
}
.scroll .scroll-holder .imageSlider {
height: 12px;
left: 12px;
padding: 0;
position: absolute;
top: 0;
transform: rotate(90deg);
transform-origin: top left;
-webkit-appearance: none;
background-color: rgba(0, 0, 0, 0);
}
.scroll .scroll-holder .imageSlider:focus {
outline: none;
}
.scroll .scroll-holder .imageSlider::-moz-focus-outer {
border: none;
}
.scroll .scroll-holder .imageSlider::-webkit-slider-runnable-track {
background-color: rgba(0, 0, 0, 0);
border: none;
cursor: pointer;
height: 5px;
z-index: 6;
}
.scroll .scroll-holder .imageSlider::-moz-range-track {
background-color: rgba(0, 0, 0, 0);
border: none;
cursor: pointer;
height: 2px;
z-index: 6;
}
.scroll .scroll-holder .imageSlider::-ms-track {
animate: 0.2s;
background: transparent;
border: none;
border-width: 15px 0;
color: rgba(0, 0, 0, 0);
cursor: pointer;
height: 12px;
width: 100%;
}
.scroll .scroll-holder .imageSlider::-ms-fill-lower {
background: rgba(0, 0, 0, 0);
}
.scroll .scroll-holder .imageSlider::-ms-fill-upper {
background: rgba(0, 0, 0, 0);
}
.scroll .scroll-holder .imageSlider::-webkit-slider-thumb {
-webkit-appearance: none !important;
background-color: #163239;
border: none;
border-radius: 57px;
cursor: -webkit-grab;
height: 12px;
margin-top: -4px;
width: 39px;
}
.scroll .scroll-holder .imageSlider::-webkit-slider-thumb:active {
background-color: #20a5d6;
cursor: -webkit-grabbing;
}
.scroll .scroll-holder .imageSlider::-moz-range-thumb {
background-color: #163239;
border: none;
border-radius: 57px;
cursor: -moz-grab;
height: 12px;
width: 39px;
z-index: 7;
}
.scroll .scroll-holder .imageSlider::-moz-range-thumb:active {
background-color: #20a5d6;
cursor: -moz-grabbing;
}
.scroll .scroll-holder .imageSlider::-ms-thumb {
background-color: #163239;
border: none;
border-radius: 57px;
cursor: ns-resize;
height: 12px;
width: 39px;
}
.scroll .scroll-holder .imageSlider::-ms-thumb:active {
background-color: #20a5d6;
}
.scroll .scroll-holder .imageSlider::-ms-tooltip {
display: none;
}
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.imageSlider {
left: 50px;
}
}
@@ -0,0 +1,67 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import './ImageScrollbar.css';
class ImageScrollbar extends PureComponent {
static propTypes = {
value: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
height: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
render() {
if (this.props.max === 0) {
return null;
}
this.style = {
width: `${this.props.height}`,
};
return (
<div className="scroll">
<div className="scroll-holder">
<input
className="imageSlider"
style={this.style}
type="range"
min="0"
max={this.props.max}
step="1"
value={this.props.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
</div>
</div>
);
}
onChange = event => {
const intValue = parseInt(event.target.value, 10);
this.props.onChange(intValue);
};
onKeyDown = event => {
// We don't allow direct keyboard up/down input on the
// image sliders since the natural direction is reversed (0 is at the top)
// Store the KeyCodes in an object for readability
const keys = {
DOWN: 40,
UP: 38,
};
// TODO: Enable scroll down / scroll up without depending on ohif-core
if (event.which === keys.DOWN) {
//OHIF.commands.run('scrollDown');
event.preventDefault();
} else if (event.which === keys.UP) {
//OHIF.commands.run('scrollUp');
event.preventDefault();
}
};
}
export default ImageScrollbar;
@@ -0,0 +1,2 @@
import ImageScrollbar from './ImageScrollbar';
export default ImageScrollbar;
@@ -4,7 +4,7 @@ import classnames from 'classnames';
import { Icon } from '../';
const MeasurementItem = ({
id,
uid,
index,
label,
displayText,
@@ -16,10 +16,10 @@ const MeasurementItem = ({
const onEditHandler = event => {
event.stopPropagation();
onEdit({ id, isActive, event });
onEdit({ uid, isActive, event });
};
const onClickHandler = event => onClick({ id, isActive, event });
const onClickHandler = event => onClick({ uid, isActive, event });
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
@@ -37,7 +37,7 @@ const MeasurementItem = ({
onClick={onClickHandler}
role="button"
tabIndex="0"
data-cy={"measurement-item"}
data-cy={'measurement-item'}
>
<div
className={classnames(
@@ -79,7 +79,7 @@ const MeasurementItem = ({
};
MeasurementItem.propTypes = {
id: PropTypes.oneOfType([
uid: PropTypes.oneOfType([
PropTypes.number.isRequired,
PropTypes.string.isRequired,
]),
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import MeasurementItem from './MeasurementItem';
const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
const { t } = useTranslation("MeasurementTable")
const { t } = useTranslation('MeasurementTable');
return (
<div>
@@ -19,8 +19,8 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
{data.length !== 0 &&
data.map((measurementItem, index) => (
<MeasurementItem
key={measurementItem.id}
id={measurementItem.id}
key={measurementItem.uid}
uid={measurementItem.uid}
index={index + 1}
label={measurementItem.label}
isActive={measurementItem.isActive}
@@ -56,7 +56,7 @@ MeasurementTable.propTypes = {
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
data: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
uid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.string,
displayText: PropTypes.arrayOf(PropTypes.string),
isActive: PropTypes.bool,
@@ -0,0 +1,151 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '../';
const SegmentationItem = ({
id,
index,
label,
displayText,
isActive,
isVisible,
onClick,
onEdit,
onDelete,
toggleVisibility,
}) => {
const [isHovering, setIsHovering] = useState(false);
const onEditHandler = event => {
event.stopPropagation();
onEdit({ id, isActive, event });
};
const onClickHandler = event => onClick({ id, isActive, event });
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
return (
<div
className={classnames(
'group relative flex cursor-pointer items-stretch bg-black border outline-none border-transparent transition duration-300',
{
'rounded overflow-hidden border-primary-light': isActive,
}
)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClickHandler}
role="button"
tabIndex="0"
data-cy={'measurement-item'}
>
<div
className={classnames(
'text-center flex items-center justify-center w-6 h-auto text-base transition duration-300',
{
'bg-primary-light text-black': isActive,
'bg-primary-dark text-primary-light group-hover:bg-secondary-main': !isActive,
}
)}
>
{isHovering ? (
<Icon
name="close"
className={classnames(
'w-5 transition duration-500 text-center hover:opacity-80',
{
'bg-primary-light text-black': isActive,
'bg-primary-dark text-primary-light group-hover:bg-secondary-main': !isActive,
}
)}
onClick={e => {
e.stopPropagation();
onDelete(id);
}}
/>
) : (
<span>{index}</span>
)}
</div>
<div className="relative flex flex-col w-full p-1">
<div className="flex items-center mb-1 ml-2">
<div className="flex items-center flex-1 text-base text-primary-light">
<div
className="w-3 h-3 mt-1 mr-2 rounded-full"
style={{ backgroundColor: 'red' }}
></div>
{label}
</div>
<div className="flex items-center w-1/4">
<div>
<Icon
className={classnames(
'text-white w-3 absolute cursor-pointer transition duration-300 hover:opacity-80',
{ 'invisible opacity-0 mr-2': !isActive && !isHovering },
{ 'visible opacity-1': !isActive && isHovering }
)}
name="pencil"
style={{
top: 7,
right: 14,
transform: isActive || isHovering ? '' : 'translateX(100%)',
}}
onClick={e => onEditHandler(e)}
/>
</div>
<div>
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isVisible ? 'eye-visible' : 'eye-hidden'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
toggleVisibility(e, id);
}}
/>
</div>
</div>
</div>
<div className="ml-3">
{displayText &&
displayText.map(line => (
<span
key={line}
className="pl-2 text-base text-white border-l border-primary-light"
>
{line}
</span>
))}
</div>
</div>
</div>
);
};
SegmentationItem.propTypes = {
id: PropTypes.oneOfType([
PropTypes.number.isRequired,
PropTypes.string.isRequired,
]),
index: PropTypes.number.isRequired,
label: PropTypes.string,
displayText: PropTypes.arrayOf(PropTypes.string),
isActive: PropTypes.bool,
isVisible: PropTypes.bool,
onClick: PropTypes.func,
onEdit: PropTypes.func,
onDelete: PropTypes.func,
toggleVisibility: PropTypes.func,
};
SegmentationItem.defaultProps = {
isActive: false,
displayText: [],
};
export default SegmentationItem;
@@ -1,120 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '../';
const SegmentationTable = ({ title, amount, data }) => {
const [activeItem, setActiveItem] = useState(null);
// TODO: colorLUT should be defined in the application
const colorLUT = [
'#932a13',
'#821393',
'#2b7fc4',
'#12c457',
'#7f7d2e',
'#7f2e4a',
'#866943',
'#65ae7f',
'#334b5f',
'#3cb0dc',
'#292ac9',
'#629753',
];
return (
<div>
<div className="flex justify-between px-2 py-1 bg-secondary-main">
<span className="text-base font-bold text-white tracking-widest uppercase">
{title}
</span>
<div className="flex">
<span className="text-base font-bold text-white">{amount}</span>
<Icon
name="eye-hidden"
className="w-6 text-white ml-2 cursor-pointer transition duration-300 hover:opacity-80"
onClick={() => alert('TBD')}
/>
</div>
</div>
<div className="overflow-y-auto overflow-x-hidden ohif-scrollbar max-h-64">
{!!data.length &&
data.map((e, i) => {
const itemKey = i;
const currentItem = i + 1;
const isActive = !!activeItem && activeItem[title] === i;
const handleOnClick = () => {
setActiveItem((s) => {
return {
...s,
[title]: s && s[title] === itemKey ? null : itemKey,
};
});
};
return (
<div
key={i}
className={classnames(
'group flex cursor-default bg-black border border-transparent transition duration-300 rounded overflow-hidden',
{
'border-primary-light': isActive,
}
)}
onClick={handleOnClick}
onKeyDown={handleOnClick}
role="button"
tabIndex="0"
>
<div
className={classnames(
'text-center w-6 py-1 text-base transition duration-300',
{
'bg-primary-light text-black': isActive,
'bg-primary-dark text-primary-light group-hover:bg-secondary-main': !isActive,
}
)}
>
{currentItem}
</div>
<div className="px-2 py-1 flex flex-1 items-center justify-between">
<span className="text-base text-primary-light mb-1 flex items-center flex-1">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: colorLUT[i] }}
></div>
Label short description
</span>
<Icon
className={classnames(
'text-white w-6 cursor-pointer transition duration-300 hover:opacity-80'
)}
name="eye-visible"
onClick={(e) => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
alert('Toggle');
}}
/>
</div>
</div>
);
})}
</div>
</div>
);
};
SegmentationTable.defaultProps = {
amount: null,
data: [],
};
SegmentationTable.propTypes = {
title: PropTypes.string.isRequired,
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
data: PropTypes.array, // TODO: define better the array structure
};
export default SegmentationTable;
@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '../';
import SegmentationItem from './SegmentationItem';
const SegmentationTable = ({
title,
amount,
segmentations,
activeSegmentationId,
onClick,
onEdit,
onDelete,
onToggleVisibility,
onToggleVisibilityAll,
}) => {
const [hiddenSegmentationIds, setHiddenSegmentationIds] = useState([]);
const handleToggleAll = () => {
// filter segmentation ids that are hidden
const visibleSegmentationsIds = segmentations
.filter(segmentation => !hiddenSegmentationIds.includes(segmentation.id))
.map(segmentation => segmentation.id);
// check if there is at least one visible segmentation
// if there is set all to invisible state
if (visibleSegmentationsIds.length > 0) {
// make all invisible
setHiddenSegmentationIds(
segmentations.map(segmentation => segmentation.id)
);
// toggle those that are visible
onToggleVisibilityAll(visibleSegmentationsIds);
}
// if there is no visible segmentation, set
// all to visible state
if (visibleSegmentationsIds.length === 0) {
// copy hidden segmentation ids
const Ids = [...hiddenSegmentationIds];
setHiddenSegmentationIds([]);
// toggle those that are hidden
onToggleVisibilityAll(Ids);
}
};
return (
<div>
<div className="flex justify-between px-2 py-1 bg-secondary-main">
<span className="text-base font-bold tracking-widest text-white uppercase">
{title}
</span>
<div className="flex">
<span className="text-base font-bold text-white">{amount}</span>
<Icon
name="eye-hidden"
className="w-6 ml-2 text-white transition duration-300 cursor-pointer hover:opacity-80"
onClick={() => handleToggleAll()}
/>
</div>
</div>
<div className="overflow-x-hidden overflow-y-auto ohif-scrollbar max-h-64">
{!!segmentations.length &&
segmentations.map((segmentation, i) => {
const { id, label, displayText = [] } = segmentation;
return (
<SegmentationItem
key={id}
id={id}
index={i + 1}
label={label ?? `Segmentation ${i + 1}`}
displayText={displayText}
isActive={activeSegmentationId === id}
isVisible={!hiddenSegmentationIds.includes(id)}
onClick={() => {
onClick(id);
}}
onEdit={() => {
onEdit(id);
}}
onDelete={() => {
onDelete(id);
}}
toggleVisibility={() => {
onToggleVisibility(id);
// if segmentation is visible, remove it from hiddenSegmentationIds
if (hiddenSegmentationIds.includes(id)) {
setHiddenSegmentationIds(
hiddenSegmentationIds.filter(hiddenId => hiddenId !== id)
);
} else {
// if segmentation is hidden, add it to hiddenSegmentationIds
setHiddenSegmentationIds([...hiddenSegmentationIds, id]);
}
}}
/>
);
})}
</div>
</div>
);
};
SegmentationTable.propTypes = {
title: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired,
segmentations: PropTypes.array.isRequired,
activeSegmentationId: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onToggleVisibility: PropTypes.func.isRequired,
onToggleVisibilityAll: PropTypes.func.isRequired,
};
SegmentationTable.defaultProps = {
title: '',
amount: 0,
segmentations: [],
activeSegmentationId: '',
onClick: () => {},
onEdit: () => {},
onToggleVisibility: () => {},
onToggleVisibilityAll: () => {},
};
export default SegmentationTable;
@@ -48,9 +48,14 @@ const Select = ({
menuPlacement,
value,
}) => {
const _noIconComponents = { DropdownIndicator: () => null, IndicatorSeparator: () => null };
const _noIconComponents = {
DropdownIndicator: () => null,
IndicatorSeparator: () => null,
};
let _components = isMulti ? { Option, MultiValue } : {};
_components = noIcons ? { ..._components, ..._noIconComponents } : _components;
_components = noIcons
? { ..._components, ..._noIconComponents }
: _components;
const selectedOptions = [];
// Map array of values to an array of selected options
@@ -123,7 +128,10 @@ Select.propTypes = {
})
),
placeholder: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.any]),
value: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.any,
]),
};
export default Select;
@@ -100,8 +100,7 @@ const SplitButton = ({
interactionType: item.type,
// splitButtonId? (so we can track group?)
// info to fire item's command/event?
commandName: item.commandName,
commandOptions: item.commandOptions,
commands: item.commands,
});
setState(state => ({
@@ -140,8 +139,7 @@ const SplitButton = ({
// splitButtonId? (so we can track group?)
// info to fire item's command/event?
//
commandName: state.primary.commandName,
commandOptions: state.primary.commandOptions,
commands: state.primary.commands,
});
};
@@ -224,7 +222,10 @@ const SplitButton = ({
className={classes.Content({ ...state })}
data-cy={`${groupId}-list-menu`}
>
<ListMenu items={state.items} renderer={(args) => renderer({ ...args, t })} />
<ListMenu
items={state.items}
renderer={args => renderer({ ...args, t })}
/>
</div>
</div>
</OutsideClickHandler>
@@ -292,7 +293,7 @@ SplitButton.propTypes = {
isActive: PropTypes.bool,
})
),
/** Callback function to inform ToolbarService of important events */
/** Callback function to inform ToolBarService of important events */
onInteraction: PropTypes.func.isRequired,
};
@@ -28,13 +28,12 @@ const StudyBrowser = ({
onClickThumbnail,
onDoubleClickThumbnail,
onClickUntrack,
activeDisplaySetInstanceUID,
activeDisplaySetInstanceUIDs,
}) => {
const { t } = useTranslation("StudyBrowser")
const { t } = useTranslation('StudyBrowser');
const getTabContent = () => {
const tabData = tabs.find(tab => tab.name === activeTabName);
return tabData.studies.map(
({
studyInstanceUid,
@@ -62,7 +61,7 @@ const StudyBrowser = ({
{isExpanded && displaySets && (
<ThumbnailList
thumbnails={displaySets}
activeDisplaySetInstanceUID={activeDisplaySetInstanceUID}
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
onThumbnailClick={onClickThumbnail}
onThumbnailDoubleClick={onDoubleClickThumbnail}
onClickUntrack={onClickUntrack}
@@ -76,7 +75,10 @@ const StudyBrowser = ({
return (
<React.Fragment>
<div className="flex flex-row items-center justify-center h-16 p-4 border-b w-100 border-secondary-light bg-primary-dark" data-cy={"studyBrowser-panel"}>
<div
className="flex flex-row items-center justify-center h-16 p-4 border-b w-100 border-secondary-light bg-primary-dark"
data-cy={'studyBrowser-panel'}
>
<ButtonGroup
variant="outlined"
color="inherit"
@@ -120,7 +122,7 @@ StudyBrowser.propTypes = {
onClickUntrack: PropTypes.func,
activeTabName: PropTypes.string.isRequired,
expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired,
activeDisplaySetInstanceUID: PropTypes.string,
activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string),
tabs: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
@@ -138,7 +140,7 @@ StudyBrowser.propTypes = {
imageSrc: PropTypes.string,
imageAltText: PropTypes.string,
seriesDate: PropTypes.string,
seriesNumber: PropTypes.number,
seriesNumber: PropTypes.string,
numInstances: PropTypes.number,
description: PropTypes.string,
componentType: PropTypes.oneOf([
@@ -167,7 +169,7 @@ StudyBrowser.propTypes = {
),
};
const noop = () => { };
const noop = () => {};
StudyBrowser.defaultProps = {
onClickTab: noop,
@@ -1,5 +1,5 @@
import StudyListTable from './StudyListTable.js';
import StudyListTableRow from './StudyListTableRow.js';
import StudyListTable from './StudyListTable.tsx';
import StudyListTableRow from './StudyListTableRow.tsx';
export default StudyListTable;
@@ -1,3 +1,3 @@
import StudySummary from './StudySummary.jsx';
import StudySummary from './StudySummary.tsx';
export default StudySummary;
@@ -24,7 +24,7 @@ const Thumbnail = ({
// this will still allow for "drag", even if there is no drop target for the
// specified item.
const [collectedProps, drag, dragPreview] = useDrag({
type: "displayset",
type: 'displayset',
item: { ...dragData },
canDrag: function(monitor) {
return Object.keys(dragData).length !== 0;
@@ -66,7 +66,7 @@ const Thumbnail = ({
<div>{imageAltText}</div>
)}
</div>
<div className="flex flex-row items-center flex-1 text-base text-blue-300 pt-2">
<div className="flex flex-row items-center flex-1 pt-2 text-base text-blue-300">
<div className="mr-4">
<span className="font-bold text-primary-main">{'S: '}</span>
{seriesNumber}
@@ -98,7 +98,7 @@ Thumbnail.propTypes = {
}),
imageAltText: PropTypes.string,
description: PropTypes.string.isRequired,
seriesNumber: PropTypes.number.isRequired,
seriesNumber: PropTypes.string.isRequired,
numInstances: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
@@ -5,10 +5,10 @@ import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '../';
const ThumbnailList = ({
thumbnails,
activeDisplaySetInstanceUID,
onThumbnailClick,
onThumbnailDoubleClick,
onClickUntrack,
activeDisplaySetInstanceUIDs = [],
}) => {
return (
<div className="py-3 bg-black overflow-y-hidden ohif-scrollbar">
@@ -29,8 +29,9 @@ const ThumbnailList = ({
imageSrc,
imageAltText,
}) => {
const isActive =
activeDisplaySetInstanceUID === displaySetInstanceUID;
const isActive = activeDisplaySetInstanceUIDs.includes(
displaySetInstanceUID
);
switch (componentType) {
case 'thumbnail':
@@ -109,7 +110,7 @@ ThumbnailList.propTypes = {
imageSrc: PropTypes.string,
imageAltText: PropTypes.string,
seriesDate: PropTypes.string,
seriesNumber: PropTypes.number,
seriesNumber: PropTypes.string,
numInstances: PropTypes.number,
description: PropTypes.string,
componentType: PropTypes.oneOf([
@@ -135,7 +136,7 @@ ThumbnailList.propTypes = {
}),
})
),
activeDisplaySetInstanceUID: PropTypes.string,
activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string),
onThumbnailClick: PropTypes.func.isRequired,
onThumbnailDoubleClick: PropTypes.func.isRequired,
onClickUntrack: PropTypes.func.isRequired,
@@ -89,7 +89,6 @@ const ThumbnailTracked = ({
</span>
)}
</div>
</div>
}
>
@@ -138,7 +137,7 @@ ThumbnailTracked.propTypes = {
imageSrc: PropTypes.string,
imageAltText: PropTypes.string,
description: PropTypes.string.isRequired,
seriesNumber: PropTypes.number.isRequired,
seriesNumber: PropTypes.string.isRequired,
numInstances: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
onDoubleClick: PropTypes.func.isRequired,
@@ -9,8 +9,7 @@ const ToolbarButton = ({
id,
icon,
label,
commandName,
commandOptions,
commands,
onInteraction,
dropdownContent,
//
@@ -49,8 +48,7 @@ const ToolbarButton = ({
onInteraction({
itemId: id,
interactionType: type,
commandName: commandName,
commandOptions: commandOptions,
commands,
});
}}
name={label}
@@ -45,10 +45,10 @@ ToolbarButton is a component that renders the ToolbarButtons.
dropdownContent: null,
isActive: false,
bState: {
primaryToolId: 'Wwwc',
primaryToolId: 'WindowLevel',
toggles: {},
groups: {
primary: 'Wwwc',
primary: 'WindowLevel',
},
},
}}
@@ -1,3 +1,3 @@
import UserPreferences from './UserPreferences.jsx';
import UserPreferences from './UserPreferences.tsx';
export default UserPreferences;
@@ -242,7 +242,7 @@ const ViewportActionBar = ({
let backgroundColor = '#020424';
if (useAltStyling || isTracked) {
backgroundColor = '#031923'
backgroundColor = '#031923';
}
return (
@@ -336,7 +336,7 @@ ViewportActionBar.propTypes = {
isTracked: PropTypes.bool.isRequired,
isRehydratable: PropTypes.bool.isRequired,
studyDate: PropTypes.string.isRequired,
currentSeries: PropTypes.number.isRequired,
currentSeries: PropTypes.string.isRequired,
seriesDescription: PropTypes.string.isRequired,
modality: PropTypes.string.isRequired,
patientInformation: PropTypes.shape({
@@ -370,7 +370,7 @@ function PatientInfo({
isOpen,
showPatientInfoRef,
}) {
const { t } = useTranslation("PatientInfo")
const { t } = useTranslation('PatientInfo');
while (patientAge.charAt(0) === '0') {
patientAge = patientAge.substr(1);
@@ -397,7 +397,9 @@ function PatientInfo({
</span>
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>{t('Sex')}</span>
<span className={classnames(classes.infoHeader)}>
{t('Sex')}
</span>
<span
className={classnames(classes.infoText)}
title={patientSex}
@@ -406,7 +408,9 @@ function PatientInfo({
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>{t('Age')}</span>
<span className={classnames(classes.infoHeader)}>
{t('Age')}
</span>
<span
className={classnames(classes.infoText)}
title={patientAge}
@@ -415,7 +419,9 @@ function PatientInfo({
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>{t('MRN')}</span>
<span className={classnames(classes.infoHeader)}>
{t('MRN')}
</span>
<span className={classnames(classes.infoText)} title={MRN}>
{MRN}
</span>
@@ -32,10 +32,11 @@ const FILE_TYPE_OPTIONS = [
];
const DEFAULT_FILENAME = 'image';
const REFRESH_VIEWPORT_TIMEOUT = 1000;
const REFRESH_VIEWPORT_TIMEOUT = 100;
const ViewportDownloadForm = ({
activeViewport,
activeViewportElement,
onClose,
updateViewportPreview,
enableViewport,
@@ -180,13 +181,13 @@ const ViewportDownloadForm = ({
const loadAndUpdateViewports = useCallback(async () => {
const { width: scaledWidth, height: scaledHeight } = await loadImage(
activeViewport,
activeViewportElement,
viewportElement,
dimensions.width,
dimensions.height
);
toggleAnnotations(showAnnotations, viewportElement);
toggleAnnotations(showAnnotations, viewportElement, activeViewportElement);
const scaledDimensions = {
height: validSize(scaledHeight),
@@ -217,7 +218,7 @@ const ViewportDownloadForm = ({
}));
}, [
loadImage,
activeViewport,
activeViewportElement,
viewportElement,
dimensions.width,
dimensions.height,
@@ -247,7 +248,7 @@ const ViewportDownloadForm = ({
loadAndUpdateViewports();
}, REFRESH_VIEWPORT_TIMEOUT);
}, [
activeViewport,
activeViewportElement,
viewportElement,
showAnnotations,
dimensions,
@@ -275,7 +276,9 @@ const ViewportDownloadForm = ({
return (
<div>
<Typography variant="h6">
{t('Please specify the dimensions, filename, and desired type for the output image.')}
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
)}
</Typography>
<div className="flex flex-col mt-6">
@@ -284,7 +287,7 @@ const ViewportDownloadForm = ({
data-cy="file-name"
value={filename}
onChange={evt => setFilename(evt.target.value)}
label={t("File Name")}
label={t('File Name')}
/>
{renderErrorHandler('filename')}
</div>
@@ -296,9 +299,11 @@ const ViewportDownloadForm = ({
type="number"
min={minimumSize}
max={maximumSize}
label={t("Image width (px)")}
label={t('Image width (px)')}
value={dimensions.width}
onChange={evt => onDimensionsChange(evt.target.value, 'width')}
onChange={evt =>
onDimensionsChange(evt.target.value, 'width')
}
data-cy="image-width"
/>
{renderErrorHandler('width')}
@@ -308,9 +313,11 @@ const ViewportDownloadForm = ({
type="number"
min={minimumSize}
max={maximumSize}
label={t("Image height (px)")}
label={t('Image height (px)')}
value={dimensions.height}
onChange={evt => onDimensionsChange(evt.target.value, 'height')}
onChange={evt =>
onDimensionsChange(evt.target.value, 'height')
}
data-cy="image-height"
/>
{renderErrorHandler('height')}
@@ -337,7 +344,7 @@ const ViewportDownloadForm = ({
<div>
<InputLabelWrapper
sortDirection="none"
label={t("File Type")}
label={t('File Type')}
isSortable={false}
onLabelClick={() => {}}
>
@@ -365,7 +372,7 @@ const ViewportDownloadForm = ({
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
<Typography>{t("Show Annotations")}</Typography>
<Typography>{t('Show Annotations')}</Typography>
</label>
</div>
</div>
@@ -373,41 +380,32 @@ const ViewportDownloadForm = ({
</div>
<div className="mt-8">
<div
className="p-4 rounded bg-secondary-dark border-secondary-primary"
data-cy="image-preview"
>
<Typography variant="h5">{t("Image preview")}</Typography>
{activeViewport && (<div
className="mx-auto my-0"
<div
className="p-4 rounded bg-secondary-dark border-secondary-primary"
data-cy="image-preview"
>
<Typography variant="h5">{t('Image preview')}</Typography>
{activeViewportElement && (
<div
className="mx-auto my-0"
style={{
height: viewportElementDimensions.height,
width: viewportElementDimensions.width,
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={classnames('block', canvasClass)}
style={{
height: downloadCanvas.height,
width: downloadCanvas.width,
}}
width={downloadCanvas.width}
height={downloadCanvas.height}
ref={downloadCanvas.ref}
></canvas>
</div>)}
{!activeViewport &&
<Typography className="mt-4">
{t("Active viewport has no displayed image")}
</Typography>
}
</div>
></div>
)}
{!activeViewportElement && (
<Typography className="mt-4">
{t('Active viewport has no displayed image')}
</Typography>
)}
</div>
</div>
<div className="flex justify-end mt-4">
<Button data-cy="cancel-btn" variant="outlined" onClick={onClose}>
{t("Cancel")}
{t('Cancel')}
</Button>
<Button
className="ml-2"
@@ -416,7 +414,7 @@ const ViewportDownloadForm = ({
color="primary"
data-cy="download-btn"
>
{t("Download")}
{t('Download')}
</Button>
</div>
</div>
@@ -1,17 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
function ViewportGrid({ numRows, numCols, children }) {
const rowSize = 100 / numRows;
const colSize = 100 / numCols;
function ViewportGrid({ numRows, numCols, layoutType, children }) {
return (
<div
data-cy="viewport-grid"
style={{
display: 'grid',
gridTemplateRows: `repeat(${numRows}, ${rowSize}%)`,
gridTemplateColumns: `repeat(${numCols}, ${colSize}%)`,
position: 'relative',
height: '100%',
width: '100%',
}}
@@ -26,6 +21,7 @@ ViewportGrid.propTypes = {
numRows: PropTypes.number.isRequired,
/** Number of rows */
numCols: PropTypes.number.isRequired,
layoutType: PropTypes.string,
/** Array of React Components to render within grid */
children: PropTypes.arrayOf(PropTypes.node).isRequired,
};
@@ -0,0 +1,49 @@
.imageViewerViewport.empty ~ .ViewportOverlay {
display: none;
}
.ViewportOverlay {
color: #9ccef9;
}
.ViewportOverlay .overlay-element {
position: absolute;
font-weight: 400;
text-shadow: 1px 1px #000;
pointer-events: none;
}
.ViewportOverlay .top-left {
top: 20px;
left: 20px;
}
.ViewportOverlay .top-center {
top: 20px;
padding-top: 20px;
width: 100%;
text-align: center;
}
.ViewportOverlay .top-right {
top: 20px;
right: 20px;
text-align: right;
}
.ViewportOverlay .bottom-left {
bottom: 20px;
left: 20px;
}
.ViewportOverlay .bottom-right {
bottom: 20px;
right: 20px;
text-align: right;
}
.ViewportOverlay.controlsVisible .topright,
.ViewportOverlay.controlsVisible .bottomright {
right: calc(20px + 19px);
}
.ViewportOverlay svg {
color: #9ccef9;
fill: #9ccef9;
stroke: #9ccef9;
background-color: transparent;
margin: 2px;
width: 18px;
height: 18px;
}
@@ -0,0 +1,43 @@
import React from 'react';
import classnames from 'classnames';
import './ViewportOverlay.css';
const ViewportOverlay = props => {
const topLeft = 'top-viewport left-viewport';
const topRight = 'top-viewport right-viewport-scrollbar';
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
const bottomLeft = 'bottom-viewport left-viewport';
const overlay = 'absolute pointer-events-none';
return (
<div className="text-primary-light">
<div
data-cy={'viewport-overlay-top-left'}
className={classnames(overlay, topLeft)}
>
{props.topLeft}
</div>
<div
data-cy={'viewport-overlay-top-right'}
className={classnames(overlay, topRight)}
>
{props.topRight}
</div>
<div
data-cy={'viewport-overlay-bottom-right'}
className={classnames(overlay, bottomRight)}
>
{props.bottomRight}
</div>
<div
data-cy={'viewport-overlay-bottom-left'}
className={classnames(overlay, bottomLeft)}
>
{props.bottomLeft}
</div>
</div>
);
};
export default ViewportOverlay;
@@ -0,0 +1,3 @@
import ViewportOverlay from './ViewportOverlay';
export default ViewportOverlay;
@@ -9,6 +9,7 @@ import { useDrop } from 'react-dnd';
function ViewportPane({
children,
className,
customStyle,
isActive,
onDrop,
onDoubleClick,
@@ -41,7 +42,7 @@ function ViewportPane({
}
};
const onInteractionHandler = (event) => {
const onInteractionHandler = event => {
focus();
onInteraction(event);
};
@@ -63,23 +64,29 @@ function ViewportPane({
onScroll={onInteractionHandler}
onWheel={onInteractionHandler}
className={classnames(
'flex flex-col',
'rounded-lg hover:border-primary-light transition duration-300 outline-none overflow-hidden',
'w-full h-full rounded-lg overflow-hidden hover:border-primary-light transition duration-300 group',
{
'border-2 border-primary-light': isActive,
'border border-secondary-light': !isActive,
'border-2 border-transparent': !isActive,
},
className
)}
// Normally, we'd use tailwindcss classes here, but margin and border classes use different units
// m-# (rem), border-# (px). To make sure we don't change the box size of our viewports
// and trigger a canvas resize, we have to use this little trick for margin.
// Assumes a :root font-fize of `16px`
style={{
margin: isActive ? '3px' : '4px',
...customStyle,
}}
>
{children}
<div
className={classnames(
'rounded-lg w-full h-full overflow-hidden group-hover:border-transparent',
{
'border border-transparent': isActive,
'border border-secondary-light': !isActive,
},
className
)}
>
{children}
</div>
</div>
);
}
@@ -101,7 +108,7 @@ ViewportPane.propTypes = {
onDoubleClick: PropTypes.func,
};
const noop = () => { };
const noop = () => {};
ViewportPane.defaultProps = {
onInteraction: noop,
+4
View File
@@ -61,6 +61,8 @@ import UserPreferences from './UserPreferences';
import HotkeysPreferences from './HotkeysPreferences';
import HotkeyField from './HotkeyField';
import Header from './Header';
import ImageScrollbar from './ImageScrollbar';
import ViewportOverlay from './ViewportOverlay';
export {
AboutModal,
@@ -87,6 +89,7 @@ export {
InputLabelWrapper,
InputMultiSelect,
InputText,
ImageScrollbar,
Label,
LayoutSelector,
MeasurementTable,
@@ -126,5 +129,6 @@ export {
ViewportDownloadForm,
ViewportGrid,
ViewportPane,
ViewportOverlay,
WindowLevelMenuItem,
};