feat: 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle (#947)

This commit is contained in:
ladeirarodolfo 2019-10-01 05:06:56 -03:00 committed by Erik Ziegler
parent ff5060f276
commit c79c0c301d
13 changed files with 389 additions and 35 deletions

View File

@ -18,6 +18,7 @@ const { BlendMode } = Constants;
// TODO: Put this somewhere else
let apis = {};
let currentSlabThickness = 0.1;
let defaultSlabThickness = 0.1;
function getCrosshairCallbackForIndex(index) {
return ({ worldPos }) => {
@ -117,6 +118,7 @@ function switchMPRInteractors(api, istyle) {
let currentSlabThickness;
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
currentSlabThickness = currentIStyle.getSlabThickness();
defaultSlabThickness = currentSlabThickness;
}
renderWindow.getInteractor().setInteractorStyle(istyle);
@ -182,7 +184,7 @@ const actions = {
switchMPRInteractors(api, istyle);
});
},
setSlabThickness: slabThickness => {
setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => {
@ -191,15 +193,6 @@ const actions = {
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
// TODO: Do this inside the interactors in a setSlabThickness function instead
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const dist = camera.getDistance();
const near = dist - currentSlabThickness / 2;
const far = dist + currentSlabThickness / 2;
camera.setClippingRange(near, far);
}
renderWindow.render();
@ -220,6 +213,32 @@ const actions = {
renderWindow.render();
});
},
setBlendModeToComposite: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
}
if (istyle.setSlabThickness) {
istyle.setSlabThickness(defaultSlabThickness);
}
renderWindow.render();
});
},
setBlendModeToMaximumIntensity: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}
renderWindow.render();
});
},
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
@ -338,12 +357,12 @@ const definitions = {
options: {},
},
setBlendModeToComposite: {
commandFn: actions.setBlendMode,
commandFn: actions.setBlendModeToComposite,
storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND },
},
setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendMode,
commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
},

View File

@ -0,0 +1,207 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Range, Checkbox, Select } from '@ohif/ui';
import './slab-thickness-toolbar-button.styl';
const SLIDER = {
MIN: 0.1,
MAX: 1000,
STEP: 0.1,
};
const ToolbarLabel = props => {
const { label } = props;
return <div className="toolbar-button-label">{label}</div>;
};
const ToolbarSlider = props => {
const { value, min, max, onChange } = props;
return (
<div className="toolbar-slider-container">
<label htmlFor="toolbar-slider">{value}mm</label>
<Range
value={value}
min={min}
max={max}
step={SLIDER.STEP}
onChange={onChange}
id="toolbar-slider"
/>
</div>
);
};
const _getSelectOptions = button => {
return button.operationButtons.map(button => {
return {
key: button.label,
value: button.id,
};
});
};
const _getClassNames = (isActive, className) => {
return classnames('toolbar-button', 'slab-thickness', className, {
active: isActive,
});
};
const _applySlabThickness = (
value,
modeChecked,
toolbarClickCallback,
button
) => {
if (!modeChecked || !toolbarClickCallback) {
return;
}
const { actionButton } = button;
const generateOperation = (operation, value) => {
// Combine slider value into slider operation
const generatedOperation = { ...operation };
generatedOperation.commandOptions = {
...operation.commandOptions,
slabThickness: value,
};
return generatedOperation;
};
const operation = generateOperation(actionButton, value);
toolbarClickCallback(operation, event);
};
const _applyModeOperation = (
operation,
modeChecked,
toolbarClickCallback,
button
) => {
// in case modeChecked has not being triggered by user yet
if (typeof modeChecked !== 'boolean') {
return;
}
const { deactivateButton } = button;
const _operation = modeChecked ? operation : deactivateButton;
if (toolbarClickCallback && _operation) {
toolbarClickCallback(_operation);
}
};
const _getInitialState = currentSelectedOption => {
return {
value: SLIDER.MIN,
sliderMin: SLIDER.MIN,
sliderMax: SLIDER.MAX,
modeChecked: undefined,
operation: currentSelectedOption,
};
};
const INITIAL_OPTION_INDEX = 0;
const _getInitialtSelectedOption = (button = {}) => {
return (
button.operationButtons && button.operationButtons[INITIAL_OPTION_INDEX]
);
};
function SlabThicknessToolbarComponent({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const currentSelectedOption = _getInitialtSelectedOption(button);
const [state, setState] = useState(_getInitialState(currentSelectedOption));
const { label, operationButtons } = button;
const _className = _getClassNames(isActive, className);
const selectOptions = _getSelectOptions(button);
function onChangeSelect(selectedValue) {
// find select value
const operation = operationButtons.find(
button => button.id === selectedValue
);
if (operation === state.operation) {
return;
}
setState({ ...state, operation });
}
function onChangeCheckbox(checked) {
setState({ ...state, modeChecked: checked });
}
function onChangeSlider(event) {
const value = Number(event.target.value);
if (value !== state.value) {
setState({ ...state, value, modeChecked: true });
}
}
useEffect(() => {
_applyModeOperation(
state.operation,
state.modeChecked,
toolbarClickCallback,
button
);
}, [state.modeChecked, state.operation]);
useEffect(() => {
_applySlabThickness(
state.value,
state.modeChecked,
toolbarClickCallback,
button
);
}, [state.operation, state.modeChecked, state.value]);
return (
<div className={_className}>
<div className="container">
<ToolbarSlider
value={state.value}
min={state.sliderMin}
max={state.sliderMax}
onChange={onChangeSlider}
/>
<ToolbarLabel key="toolbar-label" label={label} />
</div>
<div className="controller">
<Checkbox
label="mode"
checked={state.modeChecked}
onChange={onChangeCheckbox}
></Checkbox>
<Select
key="toolbar-select"
options={selectOptions}
value={selectOptions[INITIAL_OPTION_INDEX].value}
onChange={onChangeSelect}
></Select>
</div>
</div>
);
}
SlabThicknessToolbarComponent.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default SlabThicknessToolbarComponent;

View File

@ -0,0 +1,55 @@
.slab-thickness
display: flex;
flex-direction: row;
align-items: flex-end;
height: 60px;
margin-top: -12px;
&:hover
color: var(--default-color);
.container
padding-right: 10px;
height: inherit;
display: flex;
flex-direction: column;
justify-content: space-between;
.toolbar-slider-container
width: 80px;
margin: 0px 2px 2px 2px;
font-size: 12px;
&:hover
color: var(--hover-color);
input
height: 20px;
padding-top: 6px;
&::-moz-range-thumb
height: 16px;
margin-top: -7px;
&::-webkit-slider-thumb
height: 16px;
margin-top: -7px;
.controller
width: 62px;
.ohif-check-container
margin-bottom: 5px;
padding-left: 18px;
.ohif-check-label
text-transform: capitalize;
&:hover
color: var(--hover-color);
.ohif-checkbox
border-radius: 3px;
background-color: white;
width: 12px;
height: 12px;
top: 22%;
&.ohif-checked:after
background-color: var(--active-color);
top: 17%;
left: 17%;
.select-ohif
color: var(--default-color);
border-color: var(--default-color);
background-color: black;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
&:hover
color: var(--hover-color);

View File

@ -1,3 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
SET_TOOL_ACTIVE: 'setToolActive',
@ -31,6 +33,7 @@ const definitions = [
commandName: 'enableRotateTool',
commandOptions: {},
},
/*
{
id: 'setBlendModeToComposite',
label: 'Disable MIP',
@ -68,6 +71,50 @@ const definitions = [
commandName: 'decreaseSlabThickness',
commandOptions: {},
},
*/
{
id: 'changeSlabThickness',
label: 'Slab Thickness',
icon: 'soft-tissue',
CustomComponent: SlabThicknessToolbarComponent,
commandName: 'setSlabThickness',
actionButton: {
id: 'setSlabThickness',
label: 'slider',
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setSlabThickness',
commandOptions: {},
},
deactivateButton: {
id: 'setBlendModeToComposite',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToComposite',
commandOptions: {},
},
operationButtons: [
{
id: 'setBlendModeToMaximumIntensity',
label: 'MIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToMaximumIntensity',
commandOptions: {},
},
{
id: 'setBlendModeToMinimumIntensity',
label: 'MinIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToMinimumIntensity',
commandOptions: {},
},
{
id: 'setBlendModeToAverageIntensity',
label: 'AvgIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToAverageIntensity',
commandOptions: {},
},
],
},
];
export default {

View File

@ -6,15 +6,29 @@ export class Checkbox extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = { checked: props.checked, label: props.label };
this.state = { checked: !!props.checked, label: props.label };
}
handleChange(e) {
this.setState({ checked: e.target.checked });
const checked = e.target.checked;
this.setState({ checked });
if (this.props.onChange) this.props.onChange(checked);
}
componentDidUpdate(props) {
const { checked = false, label } = props;
if (this.state.checked !== checked || this.state.label !== label) {
this.setState({
checked,
label,
});
}
}
render() {
@ -32,9 +46,7 @@ export class Checkbox extends Component {
<input
type="checkbox"
checked={this.state.checked}
onChange={e => {
this.handleChange(e);
}}
onChange={this.handleChange.bind(this)}
/>
{checkbox}
{this.state.label}

View File

@ -13,6 +13,7 @@ import {
UserPreferencesModal,
} from './userPreferencesModal';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
@ -23,6 +24,7 @@ import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,

View File

@ -2,6 +2,7 @@
margin: 0;
width: 100%;
-webkit-appearance: none;
background-color: transparent;
}
.range:focus {

View File

@ -11,7 +11,7 @@ class Range extends React.Component {
handleChange = event => {
this.setState({ value: event.target.value });
if (this.props.onChange) this.props.onChange();
if (this.props.onChange) this.props.onChange(event);
};
render() {
@ -21,6 +21,7 @@ class Range extends React.Component {
value={this.state.value}
min={this.props.min}
max={this.props.max}
step={this.props.step || 1}
onChange={this.handleChange}
id={this.props.id}
className="range"

View File

@ -34,10 +34,6 @@
}
.select-ohif:focus {
border-color: #aaa;
box-shadow: 0 0 1px 3px rgba(59, 153, 252, 0.7);
box-shadow: 0 0 0 3px -moz-mac-focusring;
color: #222;
outline: none;
}

View File

@ -11,8 +11,9 @@ class Select extends Component {
}
handleChange = event => {
this.setState({ value: event.target.value });
if (this.props.onChange) this.props.onChange();
const value = event.target.value;
this.setState({ value });
if (this.props.onChange) this.props.onChange(value);
};
render() {

View File

@ -1,4 +1,5 @@
import {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
@ -25,7 +26,7 @@ import {
import { ICONS, Icon } from './elements';
// Alias this for now as not all dependents are using strict versioning
import { DropdownMenu as Dropdown } from './elements/form';
import { DropdownMenu as Dropdown, Range, Select } from './elements/form';
import ExpandableToolMenu from './viewer/ExpandableToolMenu.js';
import LayoutManager from './LayoutChooser/LayoutManager.js';
import LayoutPanelDropTarget from './LayoutChooser/LayoutPanelDropTarget.js';
@ -38,6 +39,7 @@ import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
export {
ICONS,
//
Checkbox,
CineDialog,
Dropdown,
ExpandableToolMenu,
@ -53,8 +55,10 @@ export {
OverlayTrigger,
PlayClipButton,
QuickSwitch,
Range,
RoundedButtonGroup,
ScrollableArea,
Select,
SelectTree,
SimpleDialog,
StudyBrowser,

View File

@ -108,16 +108,8 @@ class ExpandableToolMenu extends React.Component {
};
render() {
return (
<OverlayTrigger
key="menu-button"
trigger="click"
placement="bottom"
rootClose={true}
handleHide={this.onOverlayHide}
onClick={this.onExpandableToolClick}
overlay={this.toolbarMenuOverlay()}
>
const getToolBarButtonComponent = () => {
return (
<ToolbarButton
key="menu-button"
type="tool"
@ -128,6 +120,22 @@ class ExpandableToolMenu extends React.Component {
isExpandable={true}
isExpanded={this.state.isExpanded}
/>
);
};
const toolbarComponent = getToolBarButtonComponent();
return (
<OverlayTrigger
key="menu-button"
trigger="click"
placement="bottom"
rootClose={true}
handleHide={this.onOverlayHide}
onClick={this.onExpandableToolClick}
overlay={this.toolbarMenuOverlay()}
>
{toolbarComponent}
</OverlayTrigger>
);
}

View File

@ -229,6 +229,7 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
});
}
/**
* A handy way for us to handle different button types. IE. firing commands for
* buttons, or initiation built in behavior.