From 5ac9048ef08c7bbab5ff3548343a6a65771d2e34 Mon Sep 17 00:00:00 2001 From: Danny Brown Date: Thu, 4 Jul 2019 14:09:13 -0400 Subject: [PATCH] feat: extension panels * Snapshot -- Switching branches * fix classes and hooks for panel componet * Restore button functionality * feat: support for expandableToolMenu * Shift cornerstoneTools config up a layer, and use globalToolSync * Remaining initCornerstoneTools changes * Pull in Segmentation Plugin * Shift MeasurementsPanel to an extension * Note regarding where data is coming from * Make sure measurement callback is available * Bump core version * Shift MeasurementsTable to a local extensions directory * Register appCommands as an extension * Update package dependencies and yarn lock * Support for panel width * Ability to pass props to our panel component * Add a safety check around calling our tacked on method for tool options * Left sidebar plugins + fix activeIndex prop passed to sidebar. * fix: viewer height Now in a shared container w/ top bar, so we need to factor that in when calculating height * lock file latest * Update thumbnails if studies has changes * bump cornerstone version to resolve globalToolSynch history issue * Default panel to open; studies optional * Bump minor version for ohif-cornestone-extension * Simplify button logic * Accommodate odd roundedButtonGroup value change emit * Map viewers + selectedViewport instead of pulling off the window --- .../deployment/recipes/static-assets.md | 2 +- .../_ohif-example-extension/src/index.js | 42 +++- .../ohif-cornerstone-extension/package.json | 2 +- .../src/ConnectedCornerstoneViewport.js | 9 + .../src/OHIFCornerstoneViewport.js | 18 +- .../ohif-cornerstone-extension/src/config.js | 25 --- img/designs/open-graph.fig | Bin 79762 -> 79761 bytes package.json | 4 +- src/App.js | 35 ++-- src/appCommands/README.md | 1 - src/appCommands/index.js | 37 ---- .../GenericViewerCommands/commandsModule.js} | 9 +- .../GenericViewerCommands/index.js | 8 + .../ConnectedMeasurementTable.js | 6 +- src/appExtensions/MeasurementsPanel/index.js | 23 +++ .../MeasurementsPanel}/jumpToRowItem.js | 0 src/appExtensions/index.js | 4 + src/components/SidePanel.css | 40 ++++ src/components/SidePanel.js | 41 ++++ .../ConnectedFlexboxLayout.js | 16 -- .../ConnectedToolbarRow.js | 25 +-- src/connectedComponents/ConnectedViewer.js | 9 +- src/connectedComponents/FlexboxLayout.css | 46 ----- src/connectedComponents/FlexboxLayout.js | 128 ------------- src/connectedComponents/ToolbarRow.js | 181 +++++++++++------- src/connectedComponents/Viewer.js | 173 ++++++++++++++++- src/connectedComponents/ViewerMain.css | 22 +-- src/connectedComponents/ViewerMain.js | 6 +- src/index.js | 9 - src/initCornerstoneTools.js | 27 +++ src/lib/getMeasurementLocationCallback.js | 12 ++ src/setupTools.js | 12 +- src/store/layout/actions.js | 16 -- src/store/layout/reducers.js | 7 - src/variables.css | 6 +- yarn.lock | 16 +- 36 files changed, 554 insertions(+), 463 deletions(-) delete mode 100644 extensions/ohif-cornerstone-extension/src/config.js delete mode 100644 src/appCommands/README.md delete mode 100644 src/appCommands/index.js rename src/{appCommands/viewer.js => appExtensions/GenericViewerCommands/commandsModule.js} (85%) create mode 100644 src/appExtensions/GenericViewerCommands/index.js rename src/{connectedComponents => appExtensions/MeasurementsPanel}/ConnectedMeasurementTable.js (98%) create mode 100644 src/appExtensions/MeasurementsPanel/index.js rename src/{lib => appExtensions/MeasurementsPanel}/jumpToRowItem.js (100%) create mode 100644 src/appExtensions/index.js create mode 100644 src/components/SidePanel.css create mode 100644 src/components/SidePanel.js delete mode 100644 src/connectedComponents/ConnectedFlexboxLayout.js delete mode 100644 src/connectedComponents/FlexboxLayout.css delete mode 100644 src/connectedComponents/FlexboxLayout.js create mode 100644 src/initCornerstoneTools.js delete mode 100644 src/store/layout/actions.js diff --git a/docs/latest/deployment/recipes/static-assets.md b/docs/latest/deployment/recipes/static-assets.md index 01ea90eaa..4e5712f78 100644 --- a/docs/latest/deployment/recipes/static-assets.md +++ b/docs/latest/deployment/recipes/static-assets.md @@ -127,7 +127,7 @@ through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to avoid using a proxy or to simplify authentication. If that is the case, check out some of our more advanced `docker` deployments -that target these providers from the left-hand sidebar. +that target these providers from the left-hand sidepanel. These guides can be a bit longer and a update more frequently. To provide accurate documentation, we will link to each provider's own recommended steps: diff --git a/extensions/_ohif-example-extension/src/index.js b/extensions/_ohif-example-extension/src/index.js index 2d4e65b30..0ed2644f8 100644 --- a/extensions/_ohif-example-extension/src/index.js +++ b/extensions/_ohif-example-extension/src/index.js @@ -7,12 +7,28 @@ export default { */ id: 'example-extension', - getViewportModule() {}, + /** + * LIFECYCLE HOOKS + */ + + preRegistration(extensionConfiguration) {}, + + /** + * MODULE GETTERS + */ + + getViewportModule() { + return '... react component ...'; + }, getSopClassHandlerModule() { return sopClassHandlerModule; }, - getPanelModule() {}, - getToolbarModule() {}, + getPanelModule() { + return panelModule; + }, + getToolbarModule() { + return panelModule; + }, getCommandsModule(/* store */) { return commandsModule; }, @@ -67,3 +83,23 @@ const sopClassHandlerModule = { }; }, }; + +/** + * + */ +const panelModule = { + menuOptions: [ + { + icon: 'th-list', + label: 'Segments', + target: 'segment-panel', + }, + ], + components: [ + { + id: 'segment-panel', + component: '... react component ...', + }, + ], + defaultContext: ['VIEWER'], +}; diff --git a/extensions/ohif-cornerstone-extension/package.json b/extensions/ohif-cornerstone-extension/package.json index 05199b5a3..f47bab1bc 100644 --- a/extensions/ohif-cornerstone-extension/package.json +++ b/extensions/ohif-cornerstone-extension/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone", - "version": "0.0.37", + "version": "0.0.38", "description": "OHIF extension for Cornerstone", "author": "OHIF", "license": "MIT", diff --git a/extensions/ohif-cornerstone-extension/src/ConnectedCornerstoneViewport.js b/extensions/ohif-cornerstone-extension/src/ConnectedCornerstoneViewport.js index c92f5e3ee..7f1979998 100644 --- a/extensions/ohif-cornerstone-extension/src/ConnectedCornerstoneViewport.js +++ b/extensions/ohif-cornerstone-extension/src/ConnectedCornerstoneViewport.js @@ -12,6 +12,15 @@ const mapStateToProps = (state, ownProps) => { dataFromStore = state.extensions.cornerstone; } + // TODO: This should be extension configuration + // ...dataFromStore --> + // availableTools, + // onNewImage, + // onRightClick, + // onTouchPress, + // onTouchStart, + // onMouseClick, + // If this is the active viewport, enable prefetching. const { viewportIndex } = ownProps; //.viewportData; const isActive = viewportIndex === state.viewports.activeViewportIndex; diff --git a/extensions/ohif-cornerstone-extension/src/OHIFCornerstoneViewport.js b/extensions/ohif-cornerstone-extension/src/OHIFCornerstoneViewport.js index 1c1e1d789..3a75bfa0e 100644 --- a/extensions/ohif-cornerstone-extension/src/OHIFCornerstoneViewport.js +++ b/extensions/ohif-cornerstone-extension/src/OHIFCornerstoneViewport.js @@ -1,5 +1,3 @@ -import './config'; - import React, { Component } from 'react'; import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport'; @@ -20,7 +18,7 @@ cornerstone.metaData.addProvider( StackManager.setMetadataProvider(metadataProvider); const SOP_CLASSES = { - SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4' + SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4', }; const specialCaseHandlers = {}; @@ -30,11 +28,11 @@ specialCaseHandlers[ class OHIFCornerstoneViewport extends Component { state = { - viewportData: null + viewportData: null, }; static defaultProps = { - customProps: {} + customProps: {}, }; static propTypes = { @@ -42,7 +40,7 @@ class OHIFCornerstoneViewport extends Component { displaySet: PropTypes.object, viewportIndex: PropTypes.number, children: PropTypes.node, - customProps: PropTypes.object + customProps: PropTypes.object, }; static id = 'OHIFCornerstoneViewport'; @@ -185,7 +183,7 @@ class OHIFCornerstoneViewport extends Component { viewportData = { studyInstanceUid, displaySetInstanceUid, - stack + stack, }; break; @@ -201,7 +199,7 @@ class OHIFCornerstoneViewport extends Component { displaySetInstanceUid, sopClassUids, sopInstanceUid, - frameIndex + frameIndex, } = displaySet; if (sopClassUids && sopClassUids.length > 1) { @@ -221,7 +219,7 @@ class OHIFCornerstoneViewport extends Component { frameIndex ).then(viewportData => { this.setState({ - viewportData + viewportData, }); }); } @@ -252,7 +250,7 @@ class OHIFCornerstoneViewport extends Component { childrenWithProps = this.props.children.map((child, index) => { return React.cloneElement(child, { viewportIndex: this.props.viewportIndex, - key: index + key: index, }); }); } diff --git a/extensions/ohif-cornerstone-extension/src/config.js b/extensions/ohif-cornerstone-extension/src/config.js deleted file mode 100644 index b8f75e7bf..000000000 --- a/extensions/ohif-cornerstone-extension/src/config.js +++ /dev/null @@ -1,25 +0,0 @@ -import Hammer from 'hammerjs'; -import cornerstone from 'cornerstone-core'; -import cornerstoneMath from 'cornerstone-math'; -import cornerstoneTools from 'cornerstone-tools'; - -// For debugging -window.cornerstoneTools = cornerstoneTools; - -cornerstoneTools.external.cornerstone = cornerstone; -cornerstoneTools.external.Hammer = Hammer; -cornerstoneTools.external.cornerstoneMath = cornerstoneMath; -cornerstoneTools.init(); - -// Set the tool font and font size -// context.font = "[style] [variant] [weight] [size]/[line height] [font family]"; -const fontFamily = - 'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif'; -cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`); - -// Tool styles/colors -cornerstoneTools.toolStyle.setToolWidth(2); -cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)'); -cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)'); - -cornerstoneTools.store.state.touchProximity = 40; diff --git a/img/designs/open-graph.fig b/img/designs/open-graph.fig index 7b13a4b9076021168a017d269593e090634ad703..52ebfa497d0171340d0cbe86d8c15921a6e293fd 100644 GIT binary patch delta 17 YcmbRAo@L^DmWC~i`EuI} store.getState(), getActiveContexts: () => getActiveContexts(store.getState()), @@ -44,22 +52,16 @@ const commandsManager = new CommandsManager(commandsManagerConfig); const hotkeysManager = new HotkeysManager(commandsManager); const extensionManager = new ExtensionManager({ commandsManager }); -// TODO: Should be done in extensions w/ commandsModule -// ~~ ADD COMMANDS -appCommands.init(commandsManager); -if (window.config.hotkeys) { - hotkeysManager.setHotkeys(window.config.hotkeys, true); -} -// ~~~~ END APP SETUP - +// CornerstoneTools and labeling/measurements? setupTools(store); - -// const children = { -// viewport: [], -// }; +// ~~~~ END APP SETUP /** TODO: extensions should be passed in as prop as soon as we have the extensions as separate packages and then registered by ExtensionsManager */ extensionManager.registerExtensions([ + // Core + GenericViewerCommands, + MeasurementsPanel, + // OHIFCornerstoneExtension, OHIFVTKExtension, OHIFDicomPDFExtension, @@ -67,6 +69,11 @@ extensionManager.registerExtensions([ OHIFDicomMicroscopyExtension, ]); +// Must run after extension commands are registered +if (window.config.hotkeys) { + hotkeysManager.setHotkeys(window.config.hotkeys, true); +} + // TODO[react] Use a provider when the whole tree is React window.store = store; diff --git a/src/appCommands/README.md b/src/appCommands/README.md deleted file mode 100644 index 61c515e7b..000000000 --- a/src/appCommands/README.md +++ /dev/null @@ -1 +0,0 @@ -# Commands diff --git a/src/appCommands/index.js b/src/appCommands/index.js deleted file mode 100644 index 40591d5f1..000000000 --- a/src/appCommands/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import viewerCommandDefinitions from './viewer.js'; - -const CONTEXTS = { - viewer: 'VIEWER', -}; - -/** - * Register all commands. - * TODO: Extensions should self-register their commands - */ -function init(commandsManager) { - _registerViewerCommands(commandsManager); -} - -/** - * Register all Viewer commands - * - * @private - */ -function _registerViewerCommands(commandsManager) { - const commandContext = CONTEXTS.viewer; - - commandsManager.createContext(commandContext); - Object.keys(viewerCommandDefinitions).forEach(commandName => { - const commandDefinition = viewerCommandDefinitions[commandName]; - - commandsManager.registerCommand( - commandContext, - commandName, - commandDefinition - ); - }); -} - -export default { - init, -}; diff --git a/src/appCommands/viewer.js b/src/appExtensions/GenericViewerCommands/commandsModule.js similarity index 85% rename from src/appCommands/viewer.js rename to src/appExtensions/GenericViewerCommands/commandsModule.js index fa7c02772..9453c637c 100644 --- a/src/appCommands/viewer.js +++ b/src/appExtensions/GenericViewerCommands/commandsModule.js @@ -1,11 +1,11 @@ import { redux } from 'ohif-core'; -import store from './../store'; +import store from './../../store'; const { setViewportActive } = redux.actions; const actions = { updateViewportDisplaySet: ({ direction }) => { // TODO - console.warn('updateDisplaySet: ', direction); + // console.warn('updateDisplaySet: ', direction); }, updateActiveViewport: ({ viewports, direction }) => { const { viewportSpecificData, activeViewportIndex } = viewports; @@ -33,4 +33,7 @@ const definitions = { }, }; -export default definitions; +export default { + definitions, + defaultContext: 'VIEWER', +}; diff --git a/src/appExtensions/GenericViewerCommands/index.js b/src/appExtensions/GenericViewerCommands/index.js new file mode 100644 index 000000000..042fa32b1 --- /dev/null +++ b/src/appExtensions/GenericViewerCommands/index.js @@ -0,0 +1,8 @@ +import commandsModule from './commandsModule.js'; + +export default { + id: 'generic-viewer-commands', + getCommandsModule() { + return commandsModule; + }, +}; diff --git a/src/connectedComponents/ConnectedMeasurementTable.js b/src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js similarity index 98% rename from src/connectedComponents/ConnectedMeasurementTable.js rename to src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js index 9f47aefeb..f443d49ee 100644 --- a/src/connectedComponents/ConnectedMeasurementTable.js +++ b/src/appExtensions/MeasurementsPanel/ConnectedMeasurementTable.js @@ -3,8 +3,10 @@ import { MeasurementTable } from 'react-viewerbase'; import OHIF from 'ohif-core'; import moment from 'moment'; import cornerstone from 'cornerstone-core'; -import jumpToRowItem from '../lib/jumpToRowItem.js'; -import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback'; + +// +import jumpToRowItem from './jumpToRowItem.js'; +import getMeasurementLocationCallback from './../../lib/getMeasurementLocationCallback'; const { setViewportSpecificData } = OHIF.redux.actions; const { MeasurementApi } = OHIF.measurements; diff --git a/src/appExtensions/MeasurementsPanel/index.js b/src/appExtensions/MeasurementsPanel/index.js new file mode 100644 index 000000000..dc2357e24 --- /dev/null +++ b/src/appExtensions/MeasurementsPanel/index.js @@ -0,0 +1,23 @@ +import ConnectedMeasurementTable from './ConnectedMeasurementTable.js'; + +export default { + id: 'measurements-table', + getPanelModule() { + return { + menuOptions: [ + { + icon: 'list', + label: 'Measurements', + target: 'measurement-panel', + }, + ], + components: [ + { + id: 'measurement-panel', + component: ConnectedMeasurementTable, + }, + ], + defaultContext: ['VIEWER'], + }; + }, +}; diff --git a/src/lib/jumpToRowItem.js b/src/appExtensions/MeasurementsPanel/jumpToRowItem.js similarity index 100% rename from src/lib/jumpToRowItem.js rename to src/appExtensions/MeasurementsPanel/jumpToRowItem.js diff --git a/src/appExtensions/index.js b/src/appExtensions/index.js new file mode 100644 index 000000000..fbc69aab4 --- /dev/null +++ b/src/appExtensions/index.js @@ -0,0 +1,4 @@ +import GenericViewerCommands from './GenericViewerCommands/index.js'; +import MeasurementsPanel from './MeasurementsPanel/index.js'; + +export { GenericViewerCommands, MeasurementsPanel }; diff --git a/src/components/SidePanel.css b/src/components/SidePanel.css new file mode 100644 index 000000000..45325b0de --- /dev/null +++ b/src/components/SidePanel.css @@ -0,0 +1,40 @@ +.FlexboxLayout { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: stretch; + height: calc(100% - var(--toolbar-height) - var(--top-bar-height)); + width: 100%; + overflow: hidden; +} + +.sidepanel { + flex: 1; + height: 100%; + transition: var(--sidepanel-transition); +} + +.from-left { + border-right: var(--ui-border-thickness) solid var(--ui-border-color); + margin-left: calc(var(--left-sidepanel-menu-width) * -1); + max-width: var(--left-sidepanel-menu-width); +} + +.from-right { + border-left: var(--ui-border-thickness) solid var(--ui-border-color); + margin-right: calc(var(--right-sidepanel-menu-width) * -1); + max-width: var(--right-sidepanel-menu-width); +} + +.sidepanel.is-open { + margin-right: 0; + margin-left: 0; +} + +.main-content { + flex: 1; + height: 100%; + overflow: hidden; + transition: var(--sidepanel-transition); + width: 100%; +} diff --git a/src/components/SidePanel.js b/src/components/SidePanel.js new file mode 100644 index 000000000..5107f7629 --- /dev/null +++ b/src/components/SidePanel.js @@ -0,0 +1,41 @@ +import './SidePanel.css'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +class SidePanel extends Component { + static propTypes = { + from: PropTypes.string.isRequired, + isOpen: PropTypes.bool.isRequired, + children: PropTypes.node, + width: PropTypes.string, + }; + + render() { + const fromSideClass = + this.props.from === 'right' ? 'from-right' : 'from-left'; + + const styles = this.props.width + ? { + maxWidth: this.props.width, + marginRight: this.props.isOpen + ? '0' + : Number.parseInt(this.props.width) * -1, + } + : {}; + + return ( +
+ {this.props.children} +
+ ); + } +} + +export default SidePanel; diff --git a/src/connectedComponents/ConnectedFlexboxLayout.js b/src/connectedComponents/ConnectedFlexboxLayout.js deleted file mode 100644 index 75a20c8b3..000000000 --- a/src/connectedComponents/ConnectedFlexboxLayout.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; -import FlexboxLayout from './FlexboxLayout'; - -const mapStateToProps = state => { - return { - leftSidebarOpen: state.ui.leftSidebarOpen, - rightSidebarOpen: state.ui.rightSidebarOpen, - }; -}; - -const ConnectedFlexboxLayout = connect( - mapStateToProps, - null -)(FlexboxLayout); - -export default ConnectedFlexboxLayout; diff --git a/src/connectedComponents/ConnectedToolbarRow.js b/src/connectedComponents/ConnectedToolbarRow.js index 66233bbc9..3df035c3e 100644 --- a/src/connectedComponents/ConnectedToolbarRow.js +++ b/src/connectedComponents/ConnectedToolbarRow.js @@ -1,8 +1,5 @@ -import { - setLeftSidebarOpen, - setRightSidebarOpen, -} from './../store/layout/actions.js'; - +// TODO: REPLACE THIS WITH A CONTEXT PROVIDER +// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT import ToolbarRow from './ToolbarRow'; import { connect } from 'react-redux'; import { getActiveContexts } from './../store/layout/selectors.js'; @@ -10,25 +7,9 @@ import { getActiveContexts } from './../store/layout/selectors.js'; const mapStateToProps = state => { return { activeContexts: getActiveContexts(state), - leftSidebarOpen: state.ui.leftSidebarOpen, - rightSidebarOpen: state.ui.rightSidebarOpen, }; }; -const mapDispatchToProps = dispatch => { - return { - setLeftSidebarOpen: state => { - dispatch(setLeftSidebarOpen(state)); - }, - setRightSidebarOpen: state => { - dispatch(setRightSidebarOpen(state)); - }, - }; -}; - -const ConnectedToolbarRow = connect( - mapStateToProps, - mapDispatchToProps -)(ToolbarRow); +const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow); export default ConnectedToolbarRow; diff --git a/src/connectedComponents/ConnectedViewer.js b/src/connectedComponents/ConnectedViewer.js index 11ce68723..35f01f4c9 100644 --- a/src/connectedComponents/ConnectedViewer.js +++ b/src/connectedComponents/ConnectedViewer.js @@ -4,6 +4,13 @@ import OHIF from 'ohif-core'; const { setTimepoints, setMeasurements } = OHIF.redux.actions; +const mapStateToProps = (state, ownProps) => { + return { + viewports: state.viewports.viewportSpecificData, + activeViewportIndex: state.viewports.activeViewportIndex, + }; +}; + const mapDispatchToProps = dispatch => { return { onTimepointsUpdated: timepoints => { @@ -16,7 +23,7 @@ const mapDispatchToProps = dispatch => { }; const ConnectedViewer = connect( - null, + mapStateToProps, mapDispatchToProps )(Viewer); diff --git a/src/connectedComponents/FlexboxLayout.css b/src/connectedComponents/FlexboxLayout.css deleted file mode 100644 index b976200f5..000000000 --- a/src/connectedComponents/FlexboxLayout.css +++ /dev/null @@ -1,46 +0,0 @@ -.FlexboxLayout { - display: flex; - flex: 1; - flex-flow: row nowrap; - align-items: stretch; - height: calc(100% - var(--toolbar-height)); - width: 100%; - overflow: hidden; -} - -.sidebar-menu { - height: 100%; - transition: var(--sidebar-transition); -} - -.sidebar-left { - border-right: var(--ui-border-thickness) solid var(--ui-border-color); - flex: 1; - margin-left: calc(var(--left-sidebar-menu-width) * -1); - max-width: var(--left-sidebar-menu-width); - order: 1; -} - -.sidebar-left.sidebar-open { - margin-left: 0; -} - -.main-content { - flex: 1; - height: 100%; - order: 2; - overflow: hidden; - transition: var(--sidebar-transition); - width: 100%; -} - -.sidebar-right { - flex: 1; - margin-right: calc(var(--right-sidebar-menu-width) * -1); - max-width: var(--right-sidebar-menu-width); - order: 3; -} - -.sidebar-right.sidebar-open { - margin-right: 0; -} diff --git a/src/connectedComponents/FlexboxLayout.js b/src/connectedComponents/FlexboxLayout.js deleted file mode 100644 index 732e5828d..000000000 --- a/src/connectedComponents/FlexboxLayout.js +++ /dev/null @@ -1,128 +0,0 @@ -import './FlexboxLayout.css'; - -import React, { Component } from 'react'; - -import ConnectedMeasurementTable from './ConnectedMeasurementTable'; -import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; -import ConnectedViewerMain from './ConnectedViewerMain.js'; -import PropTypes from 'prop-types'; - -class FlexboxLayout extends Component { - static propTypes = { - studies: PropTypes.array, - leftSidebarOpen: PropTypes.bool.isRequired, - rightSidebarOpen: PropTypes.bool.isRequired, - }; - - state = { - studiesForBrowser: [], - }; - - componentDidMount() { - if (this.props.studies) { - const studiesForBrowser = this.getStudiesForBrowser(); - - this.setState({ - studiesForBrowser, - }); - } - } - - componentDidUpdate(prevProps) { - if (this.props.studies !== prevProps.studies) { - const studiesForBrowser = this.getStudiesForBrowser(); - - this.setState({ - studiesForBrowser, - }); - } - } - - getStudiesForBrowser = () => { - const { studies } = this.props; - - // TODO[react]: - // - Add sorting of display sets - // - Add useMiddleSeriesInstanceAsThumbnail - // - Add showStackLoadingProgressBar option - return studies.map(study => { - const { studyInstanceUid } = study; - - const thumbnails = study.displaySets.map(displaySet => { - const { - displaySetInstanceUid, - seriesDescription, - seriesNumber, - instanceNumber, - numImageFrames, - // TODO: This is undefined - // modality, - } = displaySet; - - let imageId; - let altImageText = ' '; // modality - - if (displaySet.images && displaySet.images.length) { - imageId = displaySet.images[0].getImageId(); - } else { - altImageText = 'SR'; - } - - return { - imageId, - altImageText, - displaySetInstanceUid, - seriesDescription, - seriesNumber, - instanceNumber, - numImageFrames, - }; - }); - - return { - studyInstanceUid, - thumbnails, - }; - }); - }; - - render() { - let mainContentClassName = 'main-content'; - if (this.props.leftSidebarOpen) { - mainContentClassName += ' sidebar-left-open'; - } - - if (this.props.rightSidebarOpen) { - mainContentClassName += ' sidebar-right-open'; - } - - // TODO[react]: Make ConnectedMeasurementTable extension with state.timepointManager - return ( -
-
- -
-
- -
-
- -
-
- ); - } -} - -export default FlexboxLayout; diff --git a/src/connectedComponents/ToolbarRow.js b/src/connectedComponents/ToolbarRow.js index 43a945c75..a12dde0d4 100644 --- a/src/connectedComponents/ToolbarRow.js +++ b/src/connectedComponents/ToolbarRow.js @@ -1,7 +1,11 @@ import './ToolbarRow.css'; import React, { Component } from 'react'; -import { RoundedButtonGroup, ToolbarButton } from 'react-viewerbase'; +import { + RoundedButtonGroup, + ToolbarButton, + ExpandableToolMenu, +} from 'react-viewerbase'; import { commandsManager, extensionManager } from './../App.js'; import ConnectedCineDialog from './ConnectedCineDialog'; @@ -11,19 +15,17 @@ import { MODULE_TYPES } from 'ohif-core'; import PropTypes from 'prop-types'; class ToolbarRow extends Component { + // TODO: Simplify these? isOpen can be computed if we say "any" value for selected, + // closed if selected is null/undefined static propTypes = { - leftSidebarOpen: PropTypes.bool.isRequired, - rightSidebarOpen: PropTypes.bool.isRequired, - setLeftSidebarOpen: PropTypes.func, - setRightSidebarOpen: PropTypes.func, + isLeftSidePanelOpen: PropTypes.bool.isRequired, + isRightSidePanelOpen: PropTypes.bool.isRequired, + selectedLeftSidePanel: PropTypes.string.isRequired, + selectedRightSidePanel: PropTypes.string.isRequired, + handleSidePanelChange: PropTypes.func, activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired, }; - static defaultProps = { - leftSidebarOpen: false, - rightSidebarOpen: false, - }; - constructor(props) { super(props); @@ -43,6 +45,43 @@ class ToolbarRow extends Component { }; this._handleBuiltIn = _handleBuiltIn.bind(this); + + const panelModules = extensionManager.modules[MODULE_TYPES.PANEL]; + this.buttonGroups = { + left: [ + // TODO: This should come from extensions, instead of being baked in + { + value: 'studies', + icon: 'th-large', + bottomLabel: 'Series', + }, + ], + right: [], + }; + + panelModules.forEach(panelExtension => { + const panelModule = panelExtension.module; + const defaultContexts = Array.from(panelModule.defaultContext); + + // MENU OPTIONS + panelModule.menuOptions.forEach(menuOption => { + const contexts = Array.from(menuOption.context || defaultContexts); + + const activeContextIncludesAnyPanelContexts = this.props.activeContexts.some( + actx => contexts.includes(actx) + ); + if (activeContextIncludesAnyPanelContexts) { + const menuOptionEntry = { + value: menuOption.target, + icon: menuOption.icon, + bottomLabel: menuOption.label, + }; + const from = menuOption.from || 'right'; + + this.buttonGroups[from].push(menuOptionEntry); + } + }); + }); } componentDidUpdate(prevProps) { @@ -56,39 +95,7 @@ class ToolbarRow extends Component { } } - onLeftSidebarValueChanged = value => { - this.props.setLeftSidebarOpen(!!value); - }; - - onRightSidebarValueChanged = value => { - this.props.setRightSidebarOpen(!!value); - }; - render() { - const leftSidebarToggle = [ - { - value: 'studies', - icon: 'th-large', - bottomLabel: 'Series', - }, - ]; - - const rightSidebarToggle = [ - { - value: 'measurements', - icon: 'list', - bottomLabel: 'Measurements', - }, - ]; - - const leftSidebarValue = this.props.leftSidebarOpen - ? leftSidebarToggle[0].value - : null; - - const rightSidebarValue = this.props.rightSidebarOpen - ? rightSidebarToggle[0].value - : null; - const buttonComponents = _getButtonComponents.call( this, this.state.toolbarButtons, @@ -102,14 +109,20 @@ class ToolbarRow extends Component { zIndex: 999, }; + const onPress = (side, value) => { + this.props.handleSidePanelChange(side, value); + }; + const onPressLeft = onPress.bind(this, 'left'); + const onPressRight = onPress.bind(this, 'right'); + return ( <>
{buttonComponents} @@ -119,11 +132,13 @@ class ToolbarRow extends Component { className="pull-right m-t-1 rm-x-1" style={{ marginLeft: 'auto' }} > - + {this.buttonGroups.right.length && ( + + )}
@@ -140,36 +155,64 @@ class ToolbarRow extends Component { */ function _getButtonComponents(toolbarButtons, activeButtons) { return toolbarButtons.map((button, index) => { - // TODO: If `button.buttons`, use `ExpandedToolMenu` - // I don't believe any extensions currently leverage this + if (button.buttons) { + // Iterate over button definitions and update `onClick` behavior + const childButtons = button.buttons.map(childButton => { + childButton.onClick = _handleToolbarButtonClick.bind(this, childButton); + return childButton; + }); + + return ( + + ); + } + return ( { - if (button.commandName) { - const options = Object.assign({ evt }, button.commandOptions); - commandsManager.runCommand(button.commandName, options); - } - - // TODO: Use Types ENUM - // TODO: We can update this to be a `getter` on the extension to query - // For the active tools after we apply our updates? - if (button.type === 'setToolActive') { - this.setState({ - activeButtons: [button.id], - }); - } else if (button.type === 'builtIn') { - this._handleBuiltIn(button.options); - } - }} + onClick={_handleToolbarButtonClick.bind(this, button)} isActive={activeButtons.includes(button.id)} /> ); }); } +/** + * A handy way for us to handle different button types. IE. firing commands for + * buttons, or initiation built in behavior. + * + * @param {*} button + * @param {*} evt + * @param {*} props + */ +function _handleToolbarButtonClick(button, evt, props) { + if (button.commandName) { + const options = Object.assign({ evt }, button.commandOptions); + commandsManager.runCommand(button.commandName, options); + } + + // TODO: Use Types ENUM + // TODO: We can update this to be a `getter` on the extension to query + // For the active tools after we apply our updates? + if (button.type === 'setToolActive') { + this.setState({ + activeButtons: [button.id], + }); + } else if (button.type === 'builtIn') { + this._handleBuiltIn(button.options); + } +} + +/** + * + */ function _getVisibleToolbarButtons() { const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR]; const toolbarButtonDefinitions = []; diff --git a/src/connectedComponents/Viewer.js b/src/connectedComponents/Viewer.js index 0fd4610d2..e6e900f26 100644 --- a/src/connectedComponents/Viewer.js +++ b/src/connectedComponents/Viewer.js @@ -1,15 +1,18 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -//import OHIF from 'ohif-core'; -//import { CineDialog } from 'react-viewerbase'; +import classNames from 'classnames'; +import { MODULE_TYPES } from 'ohif-core'; import OHIF from 'ohif-core'; import moment from 'moment'; import WhiteLabellingContext from '../WhiteLabellingContext.js'; import ConnectedHeader from './ConnectedHeader.js'; -import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js'; import ConnectedToolbarRow from './ConnectedToolbarRow.js'; import ConnectedLabellingOverlay from './ConnectedLabellingOverlay'; +import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; +import ConnectedViewerMain from './ConnectedViewerMain.js'; +import SidePanel from './../components/SidePanel.js'; +import { extensionManager } from './../App.js'; import './Viewer.css'; /** * Inits OHIF Hanging Protocol's onReady. @@ -53,6 +56,10 @@ class Viewer extends Component { studyInstanceUids: PropTypes.array, onTimepointsUpdated: PropTypes.func, onMeasurementsUpdated: PropTypes.func, + // window.store.getState().viewports.viewportSpecificData + viewports: PropTypes.object.isRequired, + // window.store.getState().viewports.activeViewportIndex + activeViewportIndex: PropTypes.number.isRequired, }; constructor(props) { @@ -75,6 +82,14 @@ class Viewer extends Component { }); } + state = { + isLeftSidePanelOpen: true, + isRightSidePanelOpen: false, + selectedRightSidePanel: '', + selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this + thumbnails: [], + }; + retrieveMeasurements = (patientId, timepointIds) => { OHIF.log.info('retrieveMeasurements'); // TODO: Retrieve the measurements from the latest available SR @@ -171,8 +186,13 @@ class Viewer extends Component { if (studies) { const patientId = studies[0] && studies[0].patientId; + timepointApi.retrieveTimepoints({ patientId }); measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); + + this.setState({ + thumbnails: _mapStudiesToThumbnails(studies), + }); } } @@ -181,14 +201,33 @@ class Viewer extends Component { const { studies } = this.props; const patientId = studies[0] && studies[0].patientId; const currentTimepointId = this.currentTimepointId; + this.timepointApi.retrieveTimepoints({ patientId }); this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); + + this.setState({ + thumbnails: _mapStudiesToThumbnails(studies), + }); } } render() { + let VisiblePanelLeft, VisiblePanelRight; + const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL]; + + panelExtensions.forEach(panelExt => { + panelExt.module.components.forEach(comp => { + if (comp.id === this.state.selectedRightSidePanel) { + VisiblePanelRight = comp.component; + } else if (comp.id === this.state.selectedLeftSidePanel) { + VisiblePanelLeft = comp.component; + } + }); + }); + return ( <> + {/* HEADER */} {whiteLabelling => ( @@ -196,16 +235,132 @@ class Viewer extends Component { )} -
- - {/**/} - {/**/} - - + + {/* TOOLBAR */} + { + const sideClicked = side && side[0].toUpperCase() + side.slice(1); + const openKey = `is${sideClicked}SidePanelOpen`; + const selectedKey = `selected${sideClicked}SidePanel`; + const updatedState = Object.assign({}, this.state); + + const isOpen = updatedState[openKey]; + const prevSelectedPanel = updatedState[selectedKey]; + // RoundedButtonGroup returns `null` if selected button is clicked + const isSameSelectedPanel = + prevSelectedPanel === selectedPanel || selectedPanel === null; + + updatedState[selectedKey] = selectedPanel || prevSelectedPanel; + + const isClosedOrShouldClose = !isOpen || isSameSelectedPanel; + if (isClosedOrShouldClose) { + updatedState[openKey] = !updatedState[openKey]; + } + + this.setState(updatedState); + }} + /> + + {/**/} + {/**/} + + {/* VIEWPORTS + SIDEPANELS */} +
+ {/* LEFT */} + + {VisiblePanelLeft ? ( + + ) : ( + + )} + + + {/* MAIN */} +
+ +
+ + {/* RIGHT */} + + {VisiblePanelRight && ( + + )} +
+ ); } } export default Viewer; + +/** + * What types are these? Why do we have "mapping" dropped in here instead of in + * a mapping layer? + * + * TODO[react]: + * - Add sorting of display sets + * - Add useMiddleSeriesInstanceAsThumbnail + * - Add showStackLoadingProgressBar option + * + * @param {Study[]} studies + * @param {DisplaySet[]} studies[].displaySets + */ +const _mapStudiesToThumbnails = function(studies) { + return studies.map(study => { + const { studyInstanceUid } = study; + + const thumbnails = study.displaySets.map(displaySet => { + const { + displaySetInstanceUid, + seriesDescription, + seriesNumber, + instanceNumber, + numImageFrames, + } = displaySet; + + let imageId; + let altImageText = ' '; // modality + + if (displaySet.images && displaySet.images.length) { + imageId = displaySet.images[0].getImageId(); + } else { + altImageText = 'SR'; + } + + return { + imageId, + altImageText, + displaySetInstanceUid, + seriesDescription, + seriesNumber, + instanceNumber, + numImageFrames, + }; + }); + + return { + studyInstanceUid, + thumbnails, + }; + }); +}; diff --git a/src/connectedComponents/ViewerMain.css b/src/connectedComponents/ViewerMain.css index 8d393264b..faa71b191 100644 --- a/src/connectedComponents/ViewerMain.css +++ b/src/connectedComponents/ViewerMain.css @@ -15,27 +15,7 @@ transition: all 0.3s ease; } -.ViewerMain>div { +.ViewerMain > div { width: 100%; height: 100%; } - -.ViewerMain #imageViewerViewports .viewportContainer { - /*theme('border', '%s solid $uiBorderColorDark' % $viewportBorderThickness)*/ - /*float: left; - position: relative;*/ - outline: 0; /* Prevent blue outline in Chrome */ -} - -.ViewerMain #imageViewerViewports .viewportContainer:hover, -.ViewerMain #imageViewerViewports .viewportContainer:active, -.ViewerMain #imageViewerViewports .viewportContainer:hover.active { - outline: 0; /* Prevent blue outline in Chrome */ -} - -/* &:hover - //theme('border', '%s solid $uiBorderColor' % $viewportBorderThickness) - - &.active, &:hover.active - //theme('border', '%s solid $uiBorderColorActive' % $viewportBorderThickness) -*/ diff --git a/src/connectedComponents/ViewerMain.js b/src/connectedComponents/ViewerMain.js index 02393911d..64ae2549a 100644 --- a/src/connectedComponents/ViewerMain.js +++ b/src/connectedComponents/ViewerMain.js @@ -139,9 +139,9 @@ class ViewerMain extends Component {
{this.state.displaySets.length && ( {/* Children to add to each viewport that support children */} diff --git a/src/index.js b/src/index.js index fff5f61bd..0ef2002c4 100644 --- a/src/index.js +++ b/src/index.js @@ -109,18 +109,9 @@ props.oidc = [ };*/ /* -UI settings -Plugins - - Custom tools / buttons - - Custom Sidebar module thing - - Custom Viewports - - Custom Sop Class Interpreters -*/ - /*"PUBLIC_SETTINGS": { "ui": { "studyListFunctionsEnabled": true, - "leftSidebarOpen": false, "displaySetNavigationLoopOverSeries": false, "displaySetNavigationMultipleViewports": true, "autoPositionMeasurementsTextCallOuts": "TRLB" diff --git a/src/initCornerstoneTools.js b/src/initCornerstoneTools.js new file mode 100644 index 000000000..a53f7d8e8 --- /dev/null +++ b/src/initCornerstoneTools.js @@ -0,0 +1,27 @@ +import Hammer from 'hammerjs'; +import cornerstone from 'cornerstone-core'; +import cornerstoneMath from 'cornerstone-math'; +import cornerstoneTools from 'cornerstone-tools'; + +export default function(configuration = {}) { + // For debugging + window.cornerstoneTools = cornerstoneTools; + + cornerstoneTools.external.cornerstone = cornerstone; + cornerstoneTools.external.Hammer = Hammer; + cornerstoneTools.external.cornerstoneMath = cornerstoneMath; + cornerstoneTools.init(configuration); + + // Set the tool font and font size + // context.font = "[style] [variant] [weight] [size]/[line height] [font family]"; + const fontFamily = + 'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif'; + cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`); + + // Tool styles/colors + cornerstoneTools.toolStyle.setToolWidth(2); + cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)'); + cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)'); + + cornerstoneTools.store.state.touchProximity = 40; +} diff --git a/src/lib/getMeasurementLocationCallback.js b/src/lib/getMeasurementLocationCallback.js index 38460ab60..67858782d 100644 --- a/src/lib/getMeasurementLocationCallback.js +++ b/src/lib/getMeasurementLocationCallback.js @@ -12,6 +12,18 @@ export default function getMeasurementLocationCallback( const ToolInstance = cornerstoneTools.getToolForElement(element, toolType); + if ( + !ToolInstance || + !ToolInstance.configuration || + !ToolInstance.configuration.getMeasurementLocationCallback + ) { + console.warn( + 'Tool instance configuration is missing: getMeasurementLocationCallback' + ); + + return; + } + ToolInstance.configuration.getMeasurementLocationCallback( tool, eventData, diff --git a/src/setupTools.js b/src/setupTools.js index df77f1713..9295fa0d2 100644 --- a/src/setupTools.js +++ b/src/setupTools.js @@ -147,7 +147,7 @@ export default function setupTools(store) { { name: 'Wwwc', mouseButtonMasks: [1] }, { name: 'Bidirectional', - configuration: { + props: { configuration: { getMeasurementLocationCallback: toolLabellingFlowCallback, }, @@ -156,7 +156,7 @@ export default function setupTools(store) { }, { name: 'Length', - configuration: { + props: { configuration: { getMeasurementLocationCallback: toolLabellingFlowCallback, }, @@ -165,7 +165,7 @@ export default function setupTools(store) { }, { name: 'Angle', - configuration: { + props: { configuration: { getMeasurementLocationCallback: toolLabellingFlowCallback, }, @@ -185,7 +185,7 @@ export default function setupTools(store) { }, { name: 'EllipticalRoi', - configuration: { + props: { configuration: { getMeasurementLocationCallback: toolLabellingFlowCallback, }, @@ -194,7 +194,7 @@ export default function setupTools(store) { }, { name: 'CircleRoi', - configuration: { + props: { configuration: { getMeasurementLocationCallback: toolLabellingFlowCallback, }, @@ -203,7 +203,7 @@ export default function setupTools(store) { }, { name: 'RectangleRoi', - configuration: { + props: { configuration: { getMeasurementLocationCallback: toolLabellingFlowCallback, }, diff --git a/src/store/layout/actions.js b/src/store/layout/actions.js deleted file mode 100644 index 2b08800b1..000000000 --- a/src/store/layout/actions.js +++ /dev/null @@ -1,16 +0,0 @@ -export const setLeftSidebarOpen = state => ({ - type: 'SET_LEFT_SIDEBAR_OPEN', - state, -}); - -export const setRightSidebarOpen = state => ({ - type: 'SET_RIGHT_SIDEBAR_OPEN', - state, -}); - -const actions = { - setLeftSidebarOpen, - setRightSidebarOpen, -}; - -export default actions; diff --git a/src/store/layout/reducers.js b/src/store/layout/reducers.js index b292074ef..8e75a1179 100644 --- a/src/store/layout/reducers.js +++ b/src/store/layout/reducers.js @@ -1,17 +1,10 @@ const defaultState = { - leftSidebarOpen: true, - rightSidebarOpen: false, labelling: {}, contextMenu: {}, }; const ui = (state = defaultState, action) => { switch (action.type) { - // ~ SIDEBAR - case 'SET_LEFT_SIDEBAR_OPEN': - return Object.assign({}, state, { leftSidebarOpen: action.state }); - case 'SET_RIGHT_SIDEBAR_OPEN': - return Object.assign({}, state, { rightSidebarOpen: action.state }); case 'SET_LABELLING_FLOW_DATA': { const labelling = Object.assign({}, action.labellingFlowData); diff --git a/src/variables.css b/src/variables.css index bc13b3c0e..bf0df7495 100644 --- a/src/variables.css +++ b/src/variables.css @@ -4,8 +4,8 @@ --top-bar-expanded-height: 160px; --toolbar-height: 78px; --toolbar-drawer-height: 62px; - --left-sidebar-menu-width: 307px; - --right-sidebar-menu-width: 323px; + --left-sidepanel-menu-width: 307px; + --right-sidepanel-menu-width: 323px; --study-list-padding: 8%; --study-list-padding-medium-screen: 10px; } @@ -21,7 +21,7 @@ :root { --transition-duration: 0.3s; --transition-effect: ease; - --sidebar-transition: all 0.3s ease; + --sidepanel-transition: all 0.3s ease; } /* Thicknesses */ diff --git a/yarn.lock b/yarn.lock index d2a58c69a..2e2f87fcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1214,10 +1214,10 @@ universal-user-agent "^2.0.0" url-template "^2.0.8" -"@ohif/extension-cornerstone@0.0.37": - version "0.0.37" - resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-0.0.37.tgz#d9d31409ffbc773dde17767daa02b87f461cf11a" - integrity sha512-090WPGXOYIxZjQe0mIM7SipgR3ggUZpbtWpwg7huWC0PQObIM3Juy4L8mtss23OKaKsmrmVxUaFc5NRQhs+Z+w== +"@ohif/extension-cornerstone@0.0.38": + version "0.0.38" + resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-0.0.38.tgz#c1cdf2796bef441a293bcc045f9748c7b2c1395f" + integrity sha512-xWvK04s2xwjNlIvz4BpJ5pWP8wNmW9kpud1zkywR+TFYgthH2X64Tr01VZTOh8Mi+0Xkwe5IElb4Sw2aVA4SBg== dependencies: "@babel/runtime" "^7.2.0" classnames "^2.2.6" @@ -3796,10 +3796,10 @@ cornerstone-math@^0.1.8: resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5" integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw== -cornerstone-tools@^3.13.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-3.14.1.tgz#9a15bca1c4ab96698e91b35f38b5cfa165d58032" - integrity sha512-gfYhRawZ7oZhft1klF2b6IcTciN3f05YM8v4AWT6sVcPiA+MkRPf9GVcrmUOUDsfUtg8RG0TAAO7lN9lRr92BQ== +cornerstone-tools@^3.15.1: + version "3.15.1" + resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-3.15.1.tgz#9c14a0dde32d8d8cc60e1abf3c222b681964fb9d" + integrity sha512-nD9M6cbcQJoU5XYUfbrHyi8lt9SD32pQFJ87PfIVRgMvJQA3awobcWwnXZJAD7lB7xPvO1n763FzeuQ5CqWcpQ== dependencies: "@babel/runtime" "7.1.2" cornerstone-math "0.1.7"