feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)
* feat: add initial sop class handler for SEG * feat: move segmentation service * update segmentation service methods * fix: viewport data structure * feat: Add initial render for the DICOM SEG for each displaySet * fix: various wrong architectural dependencies between services * feat: initial separate SEG display in each viewport * feat: refactore viewport action bar * initial work for SEG hydration * fix: various bugs regarding drag and dropping different viewports * fix: rendering issues for multiple seg displaysets * fix: bugs for thumbnail and hydration * feat: fix the initial segment color * update after rebase * feat: initial design for the segmentation group table * feat: initial new design for the segmentation panel * feat: segmentation panel * feat: make segmentation appear on all related viewports * fix: segmentation load bug based on functional groups * initial work for segmentation crosshairs * fix: various stylings and functionality * fix: various stylings for the seg panel * fix: overflow styles * feat: add more ui components * feat: added segmentation config * feat: add jump to segment * feat: add jump to segment for DICOM SEG viewport * fix: bugs after rebase * feat: jump in segmentation viewport * fix: mpr support for seg * feat: add more icons * feat: new icons * feat: add new side panel * feat: add segmentation config * feat: add loading indicator to ohif * feat: make hanging protocols follow matching rules for viewports * feat: enhance drag and drop to be hanging protocol aware * fix: mpr restore previous layout * fix: crosshairs toggle * fix: add auth headers to the dicom loader via dicomwebclient * fix: bug for crosshairs toggle in mpr * fix: seg viewport reusing old toolGroup * feat: add loading animation with lottie * fix: various bugs for Segmentation hydration in mpr * feat: change outline alpha to outline opacity * feat: loading indicator for seg viewport * fix: various segmentation group styles * fix: local mode for seg * feat: add animation for the highlight * fix: loading indicatro to show segment indices * feat: enhance modality drop down ui * fix: panels * fix: download form * fix: layout shift in loading indicator * fix: loading indicator to have correct values * fix: update software number * fix: image jump between MPR and default * fix: segmentation cleanup and cine service cleanup * fix: segmentation toolgroup clena up * fix: highlight interval should not trigger again * fix: issue with multiframe sorting * rename: change onSeriesChange to onArrowsClick * fix: buttons for tmtv and layout shift * fix: various bugs wrt crosshairs * fix: crosshairs re init on reset camera * fix: middle slice calculation different from cs middle reset camera * fix: reset camera should reset viewport camera * fix: loading segments in MPR mode * fix: tmtv hp back to before * fix: layout shift * feat: orientation markers for volume viewport * fix: capture for volume viewports * fix: memory leak for back to worklist * fix: loading bar bg color * fix: side panel for only one panel * fix: react select style in production * fix: various styling for segmentation groups * fix: various ui styles * feat: add new segmentation config styles * fix: hover state for segmentation item * fix: side panel layout shift * temp add panels * try to fix scrollbar for thimbnail * fix: scroll not appearing for side panels * feat: changed styles for scrollbar * feat: add cpu fallback warning * fix: webworker destroy * fix: select styles * fix: orientation marker color and position * fix: capture screenshot for volume viewports * fix: hide segment visibility on mpr * fix: reloading an already loaded seg displayset * feat: add is equal * fix: mpr jump to measurements * apply review comments * fix: optimization for the segmentation load * fix: remove unnecessary context menu and hp service reset * fix: segmentation service wrt brush settings * feat: initial work for the stack synchronization * fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements * fix: various react proptypes * fix: thumbnails for the tmtv and change default props to default params * feat: make showing loading indicator configurable and add docs * bump versions and add more docs * fix demo * update cornerstone versions * apply review comments * feat: add reference lines * fix build * fix unit tests * add reference lines icon * fix: e2e tests * fix static wado config * docs: add segmentation service docs * fix: docker build * fix: keep camera and bump versions * Try re-enabling minification to fix deploy previews Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
This commit is contained in:
1 parent
08c58b0634
commit
83ea228d86
254 files changed
+12856
-3141
No files matched your search
@@ -0,0 +1,256 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icon, InputRange, CheckBox, InputNumber } from '../';
|
||||
import classNames from 'classnames';
|
||||
import { reducer } from './segmentationConfigReducer';
|
||||
|
||||
const ActiveSegmentationConfig = ({
|
||||
config,
|
||||
dispatch,
|
||||
setRenderOutline,
|
||||
setOutlineOpacityActive,
|
||||
setOutlineWidthActive,
|
||||
setRenderFill,
|
||||
setFillAlpha,
|
||||
usePercentage,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-between text-[12px] pt-[13px] px-2">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="text-white mb-[12px]">Active</div>
|
||||
<CheckBox
|
||||
label="Outline"
|
||||
checked={config.renderOutline}
|
||||
labelClassName="text-[12px] pl-1 pt-1"
|
||||
className="mb-[9px]"
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'RENDER_OUTLINE',
|
||||
payload: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
setRenderOutline(value);
|
||||
}}
|
||||
/>
|
||||
<CheckBox
|
||||
label="Fill"
|
||||
checked={config.renderFill}
|
||||
labelClassName="text-[12px] pl-1 pt-1"
|
||||
className="mb-[9px]"
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'RENDER_FILL',
|
||||
payload: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
setRenderFill(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center col-span-2">
|
||||
<div className="text-[#b3b3b3] text-[10px] mb-[12px]">Opacity</div>
|
||||
<InputRange
|
||||
minValue={0}
|
||||
maxValue={usePercentage ? 100 : 1}
|
||||
value={
|
||||
usePercentage ? config.outlineOpacity * 100 : config.outlineOpacity
|
||||
}
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'SET_OUTLINE_OPACITY',
|
||||
payload: {
|
||||
value: value,
|
||||
},
|
||||
});
|
||||
|
||||
setOutlineOpacityActive(value);
|
||||
}}
|
||||
step={1}
|
||||
containerClassName="mt-[4px] mb-[9px]"
|
||||
inputClassName="w-[64px]"
|
||||
labelClassName="text-white text-[12px]"
|
||||
unit="%"
|
||||
/>
|
||||
<InputRange
|
||||
minValue={0}
|
||||
maxValue={usePercentage ? 100 : 1}
|
||||
value={usePercentage ? config.fillAlpha * 100 : config.fillAlpha}
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'SET_FILL_ALPHA',
|
||||
payload: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
setFillAlpha(value);
|
||||
}}
|
||||
step={1}
|
||||
containerClassName="mt-[4px] mb-[9px]"
|
||||
inputClassName="w-[64px]"
|
||||
labelClassName="text-white text-[12px]"
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-[#b3b3b3] text-[10px] mb-[12px]">Size</div>
|
||||
<InputNumber
|
||||
value={config.outlineWidthActive}
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'SET_OUTLINE_WIDTH',
|
||||
payload: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
setOutlineWidthActive(value);
|
||||
}}
|
||||
className="-mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InactiveSegmentationConfig = ({
|
||||
config,
|
||||
dispatch,
|
||||
setRenderInactiveSegmentations,
|
||||
setFillAlphaInactive,
|
||||
usePercentage,
|
||||
}) => {
|
||||
return (
|
||||
<div className="px-2">
|
||||
<CheckBox
|
||||
label="Display Inactive Segmentations"
|
||||
checked={config.renderInactiveSegmentations}
|
||||
labelClassName="text-[12px] pt-1"
|
||||
className="mb-[9px]"
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'RENDER_INACTIVE_SEGMENTATIONS',
|
||||
payload: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
setRenderInactiveSegmentations(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex pl-4 items-center space-x-2">
|
||||
<span className="text-[10px] text-[#b3b3b3]">Opacity</span>
|
||||
<InputRange
|
||||
minValue={0}
|
||||
maxValue={usePercentage ? 100 : 1}
|
||||
value={
|
||||
usePercentage
|
||||
? config.fillAlphaInactive * 100
|
||||
: config.fillAlphaInactive
|
||||
}
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'SET_FILL_ALPHA_INACTIVE',
|
||||
payload: {
|
||||
value: value,
|
||||
},
|
||||
});
|
||||
|
||||
setFillAlphaInactive(value);
|
||||
}}
|
||||
step={1}
|
||||
containerClassName="mt-[4px]"
|
||||
inputClassName="w-[64px]"
|
||||
labelClassName="text-white text-[12px]"
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SegmentationConfig = ({
|
||||
segmentationConfig,
|
||||
setFillAlpha,
|
||||
setFillAlphaInactive,
|
||||
setOutlineWidthActive,
|
||||
setOutlineOpacityActive,
|
||||
setRenderFill,
|
||||
setRenderInactiveSegmentations,
|
||||
setRenderOutline,
|
||||
}) => {
|
||||
const [config, dispatch] = React.useReducer(
|
||||
reducer,
|
||||
segmentationConfig.initialConfig
|
||||
);
|
||||
|
||||
const [isMinimized, setIsMinimized] = useState(true);
|
||||
return (
|
||||
<div className="bg-primary-dark">
|
||||
<div
|
||||
className="flex cursor-pointer items-center"
|
||||
onClick={evt => {
|
||||
evt.stopPropagation();
|
||||
setIsMinimized(!isMinimized);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="panel-group-open-close"
|
||||
className={classNames(
|
||||
'w-5 h-5 text-white transition duration-300 cursor-pointer',
|
||||
{
|
||||
'transform rotate-90': !isMinimized,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="text-[#d8d8d8] text-[12px] font-[300]">
|
||||
{'Segmentation Appearance'}
|
||||
</span>
|
||||
</div>
|
||||
{/* active segmentation */}
|
||||
{!isMinimized && (
|
||||
<div>
|
||||
<ActiveSegmentationConfig
|
||||
config={config}
|
||||
dispatch={dispatch}
|
||||
setFillAlpha={setFillAlpha}
|
||||
setOutlineWidthActive={setOutlineWidthActive}
|
||||
setOutlineOpacityActive={setOutlineOpacityActive}
|
||||
setRenderFill={setRenderFill}
|
||||
setRenderOutline={setRenderOutline}
|
||||
usePercentage={segmentationConfig.usePercentage}
|
||||
/>
|
||||
{/* A small line */}
|
||||
<div className="h-[1px] bg-[#212456] mb-[8px] mx-1"></div>
|
||||
{/* inactive segmentation */}
|
||||
<div
|
||||
onClick={() => setIsMinimized(!isMinimized)}
|
||||
className="flex items-center cursor-pointer pl-2 pb-[9px]"
|
||||
>
|
||||
<span className="text-[#d8d8d8] text-[12px] font-[300]">
|
||||
{'Inactive Segmentations'}
|
||||
</span>
|
||||
</div>
|
||||
<InactiveSegmentationConfig
|
||||
config={config}
|
||||
dispatch={dispatch}
|
||||
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
|
||||
setFillAlphaInactive={setFillAlphaInactive}
|
||||
usePercentage={segmentationConfig.usePercentage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-[6px] bg-black "></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentationConfig.propTypes = {};
|
||||
|
||||
export default SegmentationConfig;
|
||||
@@ -0,0 +1,273 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SegmentationGroupSegment from './SegmentationGroupSegment';
|
||||
import Dropdown from '../Dropdown';
|
||||
import classnames from 'classnames';
|
||||
import Icon from '../Icon';
|
||||
import IconButton from '../IconButton';
|
||||
|
||||
const AddNewSegmentRow = ({
|
||||
id,
|
||||
onConfigChange,
|
||||
onToggleSegmentationVisibility,
|
||||
onSegmentAdd,
|
||||
isVisible,
|
||||
showAddSegment,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center pl-[29px] bg-black text-primary-active hover:opacity-80 cursor-pointer text-[12px] py-1">
|
||||
{showAddSegment && (
|
||||
<div className="flex items-center" onClick={() => onSegmentAdd()}>
|
||||
<Icon name="row-add" className="w-5 h-5" />
|
||||
<div className="">Add Segment</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-grow" />
|
||||
<div
|
||||
className="flex items-center pr-2"
|
||||
onClick={() => {
|
||||
onToggleSegmentationVisibility(id);
|
||||
}}
|
||||
>
|
||||
{isVisible ? (
|
||||
<Icon name="row-show-all" className="w-5 h-5" />
|
||||
) : (
|
||||
<Icon name="row-hide-all" className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* {isSegmentationConfigOpen && (
|
||||
<SegmentationConfig onConfigChange={onConfigChange} />
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SegmentGroupHeader = ({
|
||||
isMinimized,
|
||||
onToggleMinimizeSegmentation,
|
||||
onSegmentationClick,
|
||||
id,
|
||||
label,
|
||||
isActive,
|
||||
segmentCount,
|
||||
onSegmentationEdit,
|
||||
onSegmentationDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center pr-2 pl-[3px] h-[27px] gap-2 rounded-t-md border-b border-secondary-light cursor-pointer text-[12px]',
|
||||
{
|
||||
'bg-secondary-main': isActive,
|
||||
'bg-secondary-dark': !isActive,
|
||||
}
|
||||
)}
|
||||
onClick={evt => {
|
||||
evt.stopPropagation();
|
||||
onSegmentationClick(id);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="panel-group-open-close"
|
||||
onClick={evt => {
|
||||
evt.stopPropagation();
|
||||
onToggleMinimizeSegmentation(id);
|
||||
}}
|
||||
className={classnames(
|
||||
'w-5 h-5 text-white transition duration-300 cursor-pointer',
|
||||
{
|
||||
'transform rotate-90': !isMinimized,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="text-white ">{label.toUpperCase()}</span>
|
||||
<div className="flex-grow" />
|
||||
<span className="text-white ">{segmentCount}</span>
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
id="segmentation-dropdown"
|
||||
showDropdownIcon={false}
|
||||
list={[
|
||||
{
|
||||
title: 'Rename',
|
||||
onClick: () => {
|
||||
onSegmentationEdit(id);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
onClick: () => {
|
||||
onSegmentationDelete(id);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
id={''}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="panel-group-more" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SegmentationGroup = ({
|
||||
label,
|
||||
id,
|
||||
segmentCount,
|
||||
isVisible,
|
||||
isActive,
|
||||
onSegmentClick,
|
||||
isMinimized,
|
||||
onSegmentColorClick,
|
||||
showAddSegment,
|
||||
segments,
|
||||
activeSegmentIndex,
|
||||
onSegmentAdd,
|
||||
onSegmentationClick,
|
||||
onClickSegmentColor,
|
||||
onSegmentationEdit,
|
||||
onToggleSegmentVisibility,
|
||||
onToggleSegmentationVisibility,
|
||||
onSegmentDelete,
|
||||
showSegmentDelete,
|
||||
onToggleMinimizeSegmentation,
|
||||
onSegmentationConfigChange,
|
||||
onSegmentationDelete,
|
||||
onSegmentEdit,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<SegmentGroupHeader
|
||||
id={id}
|
||||
label={label}
|
||||
isActive={isActive}
|
||||
isMinimized={isMinimized}
|
||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
segmentCount={segmentCount}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
/>
|
||||
{!isMinimized && (
|
||||
<div className="">
|
||||
<AddNewSegmentRow
|
||||
onConfigChange={onSegmentationConfigChange}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
isVisible={isVisible}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
id={id}
|
||||
showAddSegment={showAddSegment}
|
||||
/>
|
||||
<div
|
||||
className="ohif-scrollbar overflow-y-hidden"
|
||||
style={{ maxHeight: '40rem' }}
|
||||
>
|
||||
{!!segments.length &&
|
||||
segments.map(segment => {
|
||||
if (segment === undefined || segment === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
segmentIndex,
|
||||
color,
|
||||
label,
|
||||
isVisible,
|
||||
isLocked,
|
||||
} = segment;
|
||||
return (
|
||||
<div className="mb-[1px]" key={segmentIndex}>
|
||||
<SegmentationGroupSegment
|
||||
segmentationId={id}
|
||||
segmentIndex={segmentIndex}
|
||||
label={label}
|
||||
color={color}
|
||||
isActive={activeSegmentIndex === segmentIndex}
|
||||
isLocked={isLocked}
|
||||
isVisible={isVisible}
|
||||
onClick={onSegmentClick}
|
||||
onEdit={onSegmentEdit}
|
||||
onDelete={onSegmentDelete}
|
||||
showSegmentDelete={showSegmentDelete}
|
||||
onColor={onSegmentColorClick}
|
||||
onToggleVisibility={onToggleSegmentVisibility}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentationGroup.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
segments: PropTypes.array.isRequired,
|
||||
segmentCount: PropTypes.number.isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
isMinimized: PropTypes.bool.isRequired,
|
||||
activeSegmentIndex: PropTypes.number,
|
||||
onClickNewSegment: PropTypes.func.isRequired,
|
||||
onClickSegment: PropTypes.func.isRequired,
|
||||
onClickSegmentEdit: PropTypes.func.isRequired,
|
||||
onClickSegmentDelete: PropTypes.func.isRequired,
|
||||
onToggleSegmentLocked: PropTypes.func,
|
||||
onToggleSegmentVisibility: PropTypes.func.isRequired,
|
||||
onToggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
onSegmentClick: PropTypes.func.isRequired,
|
||||
showAddSegment: PropTypes.bool.isRequired,
|
||||
onSegmentAdd: PropTypes.func.isRequired,
|
||||
onSegmentationClick: PropTypes.func.isRequired,
|
||||
onClickSegmentColor: PropTypes.func.isRequired,
|
||||
onSegmentationEdit: PropTypes.func.isRequired,
|
||||
onSegmentDelete: PropTypes.func.isRequired,
|
||||
onToggleMinimizeSegmentation: PropTypes.func.isRequired,
|
||||
onSegmentationConfigChange: PropTypes.func.isRequired,
|
||||
onSegmentationDelete: PropTypes.func.isRequired,
|
||||
onSegmentEdit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SegmentationGroup.defaultProps = {
|
||||
label: '',
|
||||
segmentCount: 0,
|
||||
segments: [],
|
||||
isVisible: true,
|
||||
isMinimized: false,
|
||||
onClickNewSegment: () => {},
|
||||
onClickSegment: () => {},
|
||||
onClickSegmentEdit: () => {},
|
||||
onClickSegmentDelete: () => {},
|
||||
onToggleSegmentLocked: () => {},
|
||||
onToggleSegmentVisibility: () => {},
|
||||
onToggleSegmentationVisibility: () => {},
|
||||
onSegmentClick: () => {},
|
||||
showAddSegment: false,
|
||||
onSegmentAdd: () => {},
|
||||
onSegmentationClick: () => {},
|
||||
onClickSegmentColor: () => {},
|
||||
onSegmentationEdit: () => {},
|
||||
onSegmentDelete: () => {},
|
||||
onToggleMinimizeSegmentation: () => {},
|
||||
onSegmentationConfigChange: () => {},
|
||||
onSegmentationDelete: () => {},
|
||||
onSegmentEdit: () => {},
|
||||
};
|
||||
|
||||
export default SegmentationGroup;
|
||||
@@ -0,0 +1,242 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
const SegmentItem = ({
|
||||
segmentIndex,
|
||||
segmentationId,
|
||||
label,
|
||||
isActive,
|
||||
isVisible,
|
||||
color,
|
||||
showSegmentDelete,
|
||||
isLocked = false,
|
||||
onClick,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onColor,
|
||||
onToggleVisibility,
|
||||
onToggleLocked,
|
||||
}) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [isSegmentIndexHovering, setIsSegmentIndexHovering] = useState(false);
|
||||
|
||||
const onMouseEnter = () => setIsHovering(true);
|
||||
const onMouseLeave = () => setIsHovering(false);
|
||||
|
||||
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'group relative flex cursor-pointer bg-primary-dark transition duration-300 text-[12px] overflow-hidden border',
|
||||
{
|
||||
'border-primary-light rounded-[3px]': isActive,
|
||||
},
|
||||
{
|
||||
'border-transparent': !isActive,
|
||||
}
|
||||
)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClick(segmentationId, segmentIndex);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-cy={'segment-item'}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
'w-[27px] flex items-center justify-center border-r border-r-black text-[12px]',
|
||||
{
|
||||
'bg-primary-light text-black border-l border-l-primary-light rounded-l-sm': isActive,
|
||||
'bg-[#1d3c58] border-l border-l-[#1d3c58] rounded-l-sm':
|
||||
!isActive && isHovering,
|
||||
'bg-primary-dark text-aqua-pale':
|
||||
!isActive && !isHovering && isVisible,
|
||||
'bg-[#140e2e] opacity-60 text-[#537594]': !isActive && !isVisible,
|
||||
}
|
||||
)}
|
||||
onMouseEnter={() => setIsSegmentIndexHovering(true)}
|
||||
onMouseLeave={() => setIsSegmentIndexHovering(false)}
|
||||
>
|
||||
{isSegmentIndexHovering && showSegmentDelete ? (
|
||||
<Icon
|
||||
name="close"
|
||||
className={classnames('w-5 h-5 pr-2')}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={classnames('flex items-center pr-2 ')}>
|
||||
{segmentIndex}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center justify-between w-full pl-2 py-1 text-white border-r border-r-black relative ',
|
||||
{
|
||||
'bg-primary-dark text-primary-light': isActive,
|
||||
'bg-primary-dark text-aqua-pale': !isActive && isVisible,
|
||||
'bg-[#140e2e] opacity-60 text-[#3d5871]': !isVisible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className={classnames('flex items-baseline gap-2')}>
|
||||
<div
|
||||
className={classnames('shrink-0 w-[8px] h-[8px] rounded-full')}
|
||||
style={{ backgroundColor: cssColor }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onColor(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
<div className="">{label}</div>
|
||||
</div>
|
||||
{/* with faded background */}
|
||||
<div className="absolute right-0 bg-black/15 rounded-lg pr-[7px]">
|
||||
{!isVisible && !isHovering && (
|
||||
<div>
|
||||
<Icon
|
||||
name="row-hidden"
|
||||
className={classnames('w-5 h-5 text-[#3d5871] ')}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleVisibility(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isHovering && (
|
||||
<div className={classnames('flex items-center')}>
|
||||
<Icon
|
||||
name="row-edit"
|
||||
className={classnames('w-5 h-5', {
|
||||
'text-white': isLocked,
|
||||
'text-primary-light': !isLocked,
|
||||
})}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onEdit(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
{isVisible ? (
|
||||
<Icon
|
||||
name="row-hide"
|
||||
className={classnames('w-5 h-5', {
|
||||
'text-white': isLocked,
|
||||
'text-primary-light': !isLocked,
|
||||
})}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleVisibility(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="row-unhide"
|
||||
className={classnames('w-5 h-5', {
|
||||
'text-white': isLocked,
|
||||
'text-primary-light': !isLocked,
|
||||
})}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleVisibility(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="relative flex flex-col w-full border-t border-t-black">
|
||||
<div className="fl`ex items-center mb-1 ml-2">
|
||||
<div className="flex items-center flex-1 text-base text-primary-light">
|
||||
<div
|
||||
className={classnames(
|
||||
'w-3 h-3 mt-1 mr-2 rounded-full cursor-pointer transition duration-300 hover:opacity-80'
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onColor(segmentationId, segmentIndex);
|
||||
}}
|
||||
style={{ backgroundColor: cssColor }}
|
||||
></div>
|
||||
{label}
|
||||
</div>
|
||||
<div className="flex items-center w-1/3">
|
||||
<div className="px-1">
|
||||
<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();
|
||||
onToggleVisibility(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{onToggleLocked !== undefined ? (
|
||||
<div className="px-1">
|
||||
<Icon
|
||||
className={classnames(
|
||||
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
|
||||
)}
|
||||
name={isLocked ? 'lock' : 'dotted-circle'}
|
||||
onClick={e => {
|
||||
// stopPropagation needed to avoid disable the current active item
|
||||
e.stopPropagation();
|
||||
onToggleLocked(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="px-1">
|
||||
<Icon
|
||||
className={classnames(
|
||||
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
|
||||
)}
|
||||
name={'pencil'}
|
||||
onClick={e => {
|
||||
// stopPropagation needed to avoid disable the current active item
|
||||
e.stopPropagation();
|
||||
onEdit(segmentationId, segmentIndex);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentItem.propTypes = {
|
||||
segmentIndex: PropTypes.number.isRequired,
|
||||
segmentationId: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
// color as array
|
||||
color: PropTypes.array,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
isLocked: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onToggleVisibility: PropTypes.func.isRequired,
|
||||
onToggleLocked: PropTypes.func,
|
||||
};
|
||||
|
||||
SegmentItem.defaultProps = {
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export default SegmentItem;
|
||||
@@ -0,0 +1,173 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from '../Icon';
|
||||
import SegmentationGroup from './SegmentationGroup';
|
||||
import SegmentationConfig from './SegmentationConfig';
|
||||
|
||||
const GetSegmentationConfig = ({
|
||||
setFillAlpha,
|
||||
setFillAlphaInactive,
|
||||
setOutlineWidthActive,
|
||||
setRenderFill,
|
||||
setRenderInactiveSegmentations,
|
||||
setRenderOutline,
|
||||
setOutlineOpacityActive,
|
||||
segmentationConfig,
|
||||
}) => {
|
||||
return (
|
||||
<SegmentationConfig
|
||||
setFillAlpha={setFillAlpha}
|
||||
setFillAlphaInactive={setFillAlphaInactive}
|
||||
setOutlineWidthActive={setOutlineWidthActive}
|
||||
setOutlineOpacityActive={setOutlineOpacityActive}
|
||||
setRenderFill={setRenderFill}
|
||||
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
|
||||
setRenderOutline={setRenderOutline}
|
||||
segmentationConfig={segmentationConfig}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SegmentationGroupTable = ({
|
||||
segmentations,
|
||||
onSegmentationAdd,
|
||||
onSegmentationEdit,
|
||||
onSegmentationClick,
|
||||
onSegmentationDelete,
|
||||
showAddSegmentation,
|
||||
showAddSegment,
|
||||
onSegmentClick,
|
||||
onSegmentAdd,
|
||||
segmentationConfig,
|
||||
onSegmentDelete,
|
||||
onSegmentEdit,
|
||||
onToggleSegmentationVisibility,
|
||||
onToggleSegmentVisibility,
|
||||
onSegmentColorClick,
|
||||
isMinimized,
|
||||
onToggleMinimizeSegmentation,
|
||||
setFillAlpha,
|
||||
setFillAlphaInactive,
|
||||
setOutlineWidthActive,
|
||||
setOutlineOpacityActive,
|
||||
setRenderFill,
|
||||
setRenderInactiveSegmentations,
|
||||
setRenderOutline,
|
||||
}) => {
|
||||
return (
|
||||
<div className="font-inter font-[300]">
|
||||
<GetSegmentationConfig
|
||||
// showAddSegmentation={showAddSegmentation}
|
||||
// onSegmentationAdd={onSegmentationAdd}
|
||||
segmentationConfig={segmentationConfig}
|
||||
setFillAlpha={setFillAlpha}
|
||||
setFillAlphaInactive={setFillAlphaInactive}
|
||||
setOutlineWidthActive={setOutlineWidthActive}
|
||||
setOutlineOpacityActive={setOutlineOpacityActive}
|
||||
setRenderFill={setRenderFill}
|
||||
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
|
||||
setRenderOutline={setRenderOutline}
|
||||
/>
|
||||
<div className="flex flex-col pr-[1px] mt-1">
|
||||
{!!segmentations.length &&
|
||||
segmentations.map((segmentation, index) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
displayText = [],
|
||||
segmentCount,
|
||||
segments,
|
||||
isVisible,
|
||||
isActive,
|
||||
activeSegmentIndex,
|
||||
} = segmentation;
|
||||
return (
|
||||
<>
|
||||
<SegmentationGroup
|
||||
id={id}
|
||||
label={label}
|
||||
isMinimized={isMinimized[id]}
|
||||
segments={segments}
|
||||
showAddSegment={showAddSegment}
|
||||
segmentCount={segmentCount}
|
||||
isActive={isActive}
|
||||
isVisible={isVisible}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentationClick={() => onSegmentationClick(id)}
|
||||
activeSegmentIndex={activeSegmentIndex}
|
||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentationVisibility={
|
||||
onToggleSegmentationVisibility
|
||||
}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
showSegmentDelete={false}
|
||||
/>
|
||||
<div className="h-2 bg-black"></div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{showAddSegmentation && (
|
||||
<div
|
||||
className="flex items-center cursor-pointer hover:opacity-80 text-primary-active bg-black text-[12px] pl-1 h-[45px]"
|
||||
onClick={() => onSegmentationAdd()}
|
||||
>
|
||||
<Icon name="row-add" className="w-5 h-5" />
|
||||
<div className="pl-1">Add New Segmentation</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentationGroupTable.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
segmentations: PropTypes.array.isRequired,
|
||||
activeSegmentationId: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onToggleLocked: PropTypes.func,
|
||||
onToggleVisibility: PropTypes.func.isRequired,
|
||||
onToggleVisibilityAll: PropTypes.func.isRequired,
|
||||
segmentationConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
SegmentationGroupTable.defaultProps = {
|
||||
title: '',
|
||||
segmentations: [],
|
||||
activeSegmentationId: '',
|
||||
onClick: () => {},
|
||||
onEdit: () => {},
|
||||
onDelete: () => {},
|
||||
onToggleLocked: () => {},
|
||||
onToggleVisibility: () => {},
|
||||
onToggleVisibilityAll: () => {},
|
||||
segmentationConfig: {
|
||||
initialConfig: {
|
||||
fillAlpha: 0.5,
|
||||
fillAlphaInactive: 0.5,
|
||||
outlineWidthActive: 1,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.5,
|
||||
renderFill: true,
|
||||
renderInactiveSegmentations: true,
|
||||
renderOutline: true,
|
||||
},
|
||||
usePercentage: true,
|
||||
},
|
||||
setFillAlpha: () => {},
|
||||
setFillAlphaInactive: () => {},
|
||||
setOutlineWidthActive: () => {},
|
||||
setOutlineOpacityActive: () => {},
|
||||
setRenderFill: () => {},
|
||||
setRenderInactiveSegmentations: () => {},
|
||||
setRenderOutline: () => {},
|
||||
};
|
||||
|
||||
export default SegmentationGroupTable;
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import SegmentationGroupTable from '../SegmentationGroupTable';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: SegmentationGroupTable,
|
||||
title: 'Components/SegmentationGroupTable',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/SegmentationGroupTable"
|
||||
component={SegmentationGroupTable}
|
||||
/>
|
||||
|
||||
export const SegmentationGroupTableTemplate = args => (
|
||||
<div className="w-64 h-96 bg-primary-dark">
|
||||
<SegmentationGroupTable {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="SegmentationGroupTable"
|
||||
componentRelativePath="SegmentationGroupTable/SegmentationGroupTable.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
SegmentationGroupTable is a component that renders the SegmentationGroupTables.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
activeSegmentationId: 'ed8d6f8d-4035-438d-9032-36d5912aaa3a',
|
||||
showAddSegmentation: true,
|
||||
isMinimized: {"4be5-b9b2-ed33-fa86970568e7": false},
|
||||
showAddSegment: true,
|
||||
segmentations: [
|
||||
{
|
||||
"id": "4be5-b9b2-ed33-fa86970568e7",
|
||||
"volumeId": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
|
||||
"activeSegmentIndex": 1,
|
||||
"cachedStats": {},
|
||||
"label": "Segmentation",
|
||||
"segmentsLocked": {},
|
||||
"type": "LABELMAP",
|
||||
"displayText": [],
|
||||
"hydrated": true,
|
||||
"segmentCount": 3,
|
||||
"segments": [
|
||||
null,
|
||||
{
|
||||
"label": "Segment 1",
|
||||
"segmentIndex": 1,
|
||||
"color": [
|
||||
128,
|
||||
174,
|
||||
128
|
||||
],
|
||||
"opacity": 255,
|
||||
"isVisible": true,
|
||||
"isLocked": false,
|
||||
},
|
||||
{
|
||||
"label": "Segment 2",
|
||||
"segmentIndex": 2,
|
||||
"color": [
|
||||
241,
|
||||
214,
|
||||
145
|
||||
],
|
||||
"opacity": 255,
|
||||
"isVisible": true,
|
||||
"isLocked": false,
|
||||
},
|
||||
{
|
||||
"label": "Segment 3",
|
||||
"segmentIndex": 3,
|
||||
"color": [
|
||||
141,
|
||||
114,
|
||||
145
|
||||
],
|
||||
"opacity": 255,
|
||||
"isVisible": false,
|
||||
"isLocked": false
|
||||
}
|
||||
],
|
||||
"isActive": false,
|
||||
"colorLUTIndex": 1,
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
|
||||
"volumeId": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
|
||||
"activeSegmentIndex": 1,
|
||||
"cachedStats": {},
|
||||
"label": "Segmentation2",
|
||||
"segmentsLocked": {},
|
||||
"type": "LABELMAP",
|
||||
"displayText": [],
|
||||
"hydrated": true,
|
||||
"segmentCount": 2,
|
||||
"segments": [
|
||||
null,
|
||||
{
|
||||
"label": "Segment 1",
|
||||
"segmentIndex": 1,
|
||||
"color": [
|
||||
228,
|
||||
174,
|
||||
128
|
||||
],
|
||||
"opacity": 255,
|
||||
"isVisible": true,
|
||||
"isLocked": false,
|
||||
},
|
||||
],
|
||||
"isActive": true,
|
||||
"colorLUTIndex": 1,
|
||||
"isVisible": true
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
{SegmentationGroupTableTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={SegmentationGroupTable} />
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="SegmentationGroupTable/__stories__/SegmentationGroupTable.stories.mdx" />
|
||||
@@ -0,0 +1,3 @@
|
||||
import SegmentationGroupTable from './SegmentationGroupTable';
|
||||
|
||||
export default SegmentationGroupTable;
|
||||
@@ -0,0 +1,22 @@
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'RENDER_OUTLINE':
|
||||
return { ...state, renderOutline: action.payload.value };
|
||||
case 'RENDER_FILL':
|
||||
return { ...state, renderFill: action.payload.value };
|
||||
case 'SET_OUTLINE_OPACITY':
|
||||
return { ...state, outlineOpacity: action.payload.value };
|
||||
case 'SET_OUTLINE_WIDTH':
|
||||
return { ...state, outlineWidth: action.payload.value };
|
||||
case 'SET_FILL_ALPHA':
|
||||
return { ...state, fillAlpha: action.payload.value };
|
||||
case 'SET_FILL_ALPHA_INACTIVE':
|
||||
return { ...state, fillAlphaInactive: action.payload.value };
|
||||
case 'RENDER_INACTIVE_SEGMENTATIONS':
|
||||
return { ...state, renderInactiveSegmentations: action.payload.value };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { reducer };
|
||||
Reference in new issue
Block a user