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

-1
View File
@@ -13,7 +13,6 @@ module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
@@ -0,0 +1,15 @@
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="edit-patient" stroke="currentColor" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Group-23" transform="translate(1.000000, 1.000000)" stroke="currentColor">
<g id="Group-21" stroke-width="1.5">
<path d="M2.459,13.4115596 C3.78406799,12.6432192 5.19358453,12.0307708 6.65943386,11.5864311 C7.30893749,11.3458742 7.37955258,9.85674938 6.91395858,9.34381999 C6.24195124,8.60507751 5.67237458,7.73829669 5.67237458,5.64622765 C5.59319579,4.80314387 5.87912888,3.96655091 6.45776749,3.3482978 C7.03640611,2.7300447 7.85226135,2.3894192 8.69873558,2.41267733 C9.5452098,2.3894192 10.361065,2.7300447 10.9397037,3.3482978 C11.5183423,3.96655091 11.8042754,4.80314387 11.7250966,5.64622765 C11.7250966,7.74140065 11.1555199,8.60507751 10.4835126,9.34381999 C10.0179186,9.85674938 10.0885337,11.3458742 10.7380373,11.5864311 C12.2038866,12.0307708 13.6134032,12.6432192 14.9384712,13.4115596" id="Path"></path>
<path d="M18,11 L18,1 C18,0.44771525 17.5522847,0 17,0 L1,0 C0.44771525,0 0,0.44771525 0,1 L0,17 C0,17.5522847 0.44771525,18 1,18 L11,18" id="Path"></path>
</g>
<g id="Group-22" transform="translate(13.000000, 13.020297)" stroke-width="1.25">
<path d="M3.5,8.97970257 L0,9.97970257 L1,6.47970257 L6.982,0.497702567 C7.67552478,-0.173658884 8.77933392,-0.164697523 9.46186701,0.517835561 C10.1444001,1.20036865 10.1533615,2.30417778 9.482,2.99770257 L3.5,8.97970257 Z" id="Path"></path>
<line x1="6.232" y1="1.24770257" x2="8.732" y2="3.74770257" id="Path"></line>
<line x1="1" y1="6.47970257" x2="3.5" y2="8.97970257" id="Path"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,9 @@
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="create-threshold" stroke="currentColor" stroke-width="1" fill="none" fill-rule="evenodd">
<polyline id="Path-3" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" points="18.85656 16.2174437 21.3536565 13.7203472 23.850753 16.2174437"></polyline>
<polyline id="Path-3" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" transform="translate(21.353657, 21.471076) scale(1, -1) translate(-21.353657, -21.471076) " points="18.85656 22.7196245 21.3536565 20.222528 23.850753 22.7196245"></polyline>
<path d="M21.8903104,6.56040847 L11.830485,16.6215711 C11.7372012,16.7144344 11.6185826,16.7776976 11.4894966,16.8034316 L7.65571758,17.5696525 C7.43667607,17.6132586 7.21032569,17.5446155 7.05240058,17.3866904 C6.89447547,17.2287653 6.8258324,17.0024149 6.86943853,16.7833734 L7.63565944,12.9509316 C7.66176759,12.8216917 7.72550512,12.7030561 7.81885711,12.6099433 L17.8786825,2.54878063 C18.9836118,1.44420272 20.7746834,1.44420272 21.8796127,2.54878063 L21.8903104,2.56081551 C22.9947353,3.66528595 22.9947353,5.45593803 21.8903104,6.56040847 Z" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<line x1="9.60946699" y1="12.6534866" x2="11.9466763" y2="14.9906958" id="Line" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"></line>
<path d="M-5.52848657,13.180889 L-5.52848657,15.180889 C-5.52848657,16.7687077 -4.24130523,18.055889 -2.65348657,18.055889 L-1.21495403,18.055889 C-0.731704876,18.055889 -0.339954032,17.6641382 -0.339954032,17.180889 C-0.339954032,16.6976399 -0.731704876,16.305889 -1.21495403,16.305889 L-2.65348657,16.305889 C-3.27480692,16.305889 -3.77848657,15.8022094 -3.77848657,15.180889 L-3.77848657,13.180889 C-3.77848657,12.6976399 -4.17023742,12.305889 -4.65348657,12.305889 C-5.13673573,12.305889 -5.52848657,12.6976399 -5.52848657,13.180889 Z M3.03504597,18.055889 L9.53504597,18.055889 C10.0182951,18.055889 10.410046,17.6641382 10.410046,17.180889 C10.410046,16.6976399 10.0182951,16.305889 9.53504597,16.305889 L3.03504597,16.305889 C2.55179681,16.305889 2.16004597,16.6976399 2.16004597,17.180889 C2.16004597,17.6641382 2.55179681,18.055889 3.03504597,18.055889 Z M13.785046,18.055889 L15.180889,18.055889 C16.7687077,18.055889 18.055889,16.7687077 18.055889,15.180889 L18.055889,13.1381996 C18.055889,12.6549504 17.6641382,12.2631996 17.180889,12.2631996 C16.6976399,12.2631996 16.305889,12.6549504 16.305889,13.1381996 L16.305889,15.180889 C16.305889,15.8022094 15.8022094,16.305889 15.180889,16.305889 L13.785046,16.305889 C13.3017968,16.305889 12.910046,16.6976399 12.910046,17.180889 C12.910046,17.6641382 13.3017968,18.055889 13.785046,18.055889 Z M18.055889,8.88819957 L18.055889,6.65348657 C18.055889,6.17023742 17.6641382,5.77848657 17.180889,5.77848657 C16.6976399,5.77848657 16.305889,6.17023742 16.305889,6.65348657 L16.305889,8.88819957 C16.305889,9.37144872 16.6976399,9.76319957 17.180889,9.76319957 C17.6641382,9.76319957 18.055889,9.37144872 18.055889,8.88819957 Z" id="Path-4" fill="currentColor" fill-rule="nonzero" transform="translate(6.263701, 11.917188) scale(1, -1) rotate(90.000000) translate(-6.263701, -11.917188) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" transform="translate(1 1)">
<path
d="m 11,10.631008 c 0.23,0 0.417,0.187 0.417,0.417 0,0.23 -0.187,0.417 -0.417,0.417 -0.23,0 -0.417,-0.187 -0.417,-0.417 0,-0.23 0.187,-0.417 0.417,-0.417"/>
<path
d="m 7.8402736,11 -7.24425567,-2e-6"/>
<path
d="m 21.403981,11 -7.244255,-2e-6"/>
<path
d="m 10.922934,0.59601755 -10e-7,7.24425465"/>
<path
d="m 10.922934,14.255744 -10e-7,7.244255"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 618 B

@@ -0,0 +1,13 @@
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="currentColor" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M18,2 C19.1045695,2 20,2.8954305 20,4 L20.000443,11.5700239 C18.8210504,10.0206853 17.6279451,8.77097591 17.1182222,8.25530435 C16.9570959,8.09231818 16.733654,8.00006956 16.5,8.00006956 C16.266346,8.00006956 16.0429041,8.09231818 15.8817778,8.25530435 C14.719,9.43165217 10,14.4278261 10,18.0869565 C10,18.7674866 10.1126915,19.4091677 10.3210825,20.0009348 L4,20 C2.8954305,20 2,19.1045695 2,18 L2,4 C2,2.8954305 2.8954305,2 4,2 L18,2 Z" id="Combined-Shape" stroke="currentColor" stroke-width="1.75"></path>
<g id="color-drop-pick" transform="translate(9.591386, 8.000070)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M7.52683573,0.255234787 C7.36570938,0.0922486197 7.14226748,0 6.90861351,0 C6.67495954,0 6.45151764,0.0922486197 6.29039129,0.255234787 C5.12761351,1.43158261 0.408613509,6.42775653 0.408613509,10.086887 C0.408613509,13.5449739 3.31844684,15.9999304 6.90861351,15.9999304 C10.4987802,15.9999304 13.4086135,13.5449739 13.4086135,10.086887 C13.4086135,6.42775653 8.68961351,1.43158261 7.52683573,0.255234787 Z" id="Path" stroke-width="1.75"></path>
<g id="Group" transform="translate(7.015298, 8.975291) rotate(40.000000) translate(-7.015298, -8.975291) translate(3.731654, 1.974720)">
<line x1="0.5" y1="12.5" x2="0.5" y2="1.56125113e-15" id="Path"></line>
<line x1="3.420153" y1="1.25018584" x2="3.420153" y2="14.0011428" id="Path"></line>
<line x1="6.06728889" y1="1.86765717" x2="6.06728889" y2="12.7034832" id="Path"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -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,
};
@@ -13,7 +13,7 @@ const DEFAULT_STATE = {
/*
* 1: { isPlaying: false, frameRate: 24 };
*/
}
},
};
const DEFAULT_CINE = { isPlaying: false, frameRate: 24 };
@@ -29,7 +29,8 @@ export default function CineProvider({ children, service }) {
if (!cines[id]) cines[id] = { id, ...DEFAULT_CINE };
cines[id].frameRate = frameRate || cines[id].frameRate;
cines[id].isPlaying = isPlaying !== undefined ? isPlaying : cines[id].isPlaying;
cines[id].isPlaying =
isPlaying !== undefined ? isPlaying : cines[id].isPlaying;
return { ...state, ...{ cines } };
}
@@ -41,15 +42,13 @@ export default function CineProvider({ children, service }) {
}
};
const [state, dispatch] = useReducer(
reducer,
DEFAULT_STATE
);
const [state, dispatch] = useReducer(reducer, DEFAULT_STATE);
const getState = useCallback(() => state, [state]);
const setIsCineEnabled = useCallback(
isCineEnabled => dispatch({ type: 'SET_IS_CINE_ENABLED', payload: isCineEnabled }),
isCineEnabled =>
dispatch({ type: 'SET_IS_CINE_ENABLED', payload: isCineEnabled }),
[dispatch]
);
@@ -75,23 +74,19 @@ export default function CineProvider({ children, service }) {
if (service) {
service.setServiceImplementation({ getState, setIsCineEnabled, setCine });
}
}, [
getState,
service,
setCine,
setIsCineEnabled,
]);
}, [getState, service, setCine, setIsCineEnabled]);
const api = {
getState,
setCine,
setIsCineEnabled,
playClip: (element, playClipOptions) =>
service.playClip(element, playClipOptions),
stopClip: element => service.stopClip(element),
};
return (
<CineContext.Provider value={[state, api]}>
{children}
</CineContext.Provider>
<CineContext.Provider value={[state, api]}>{children}</CineContext.Provider>
);
}
@@ -52,17 +52,6 @@ const DialogProvider = ({ children, service }) => {
};
};
/**
* Sets the implementation of a dialog service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ create, dismiss, dismissAll });
}
}, [create, dismiss, service]);
/**
* UI Dialog
*
@@ -80,8 +69,6 @@ const DialogProvider = ({ children, service }) => {
* @property {Function} onDrag Called while dragging.
*/
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
/**
* Creates a new dialog and return its id.
*
@@ -146,6 +133,19 @@ const DialogProvider = ({ children, service }) => {
});
}, []);
/**
* Sets the implementation of a dialog service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ create, dismiss, dismissAll });
}
}, [create, dismiss, service]);
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
const renderDialogs = () =>
dialogs.map(dialog => {
const {
@@ -27,17 +27,6 @@ const SnackbarProvider = ({ children, service }) => {
const [count, setCount] = useState(1);
const [snackbarItems, setSnackbarItems] = useState([]);
/**
* Sets the implementation of a notification service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [service, hide, show]);
const show = useCallback(
options => {
if (!options || (!options.title && !options.message)) {
@@ -104,6 +93,17 @@ const SnackbarProvider = ({ children, service }) => {
};
}
/**
* Sets the implementation of a notification service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [service, hide, show]);
return (
<SnackbarContext.Provider value={{ show, hide, hideAll, snackbarItems }}>
{!!snackbarItems && <SnackbarContainer />}
Loaded 100 of 668 files, more files were not shown because too many files have changed in this diff. Show more