feat(segmentation mode): Add create, and export SEG with Brushes (#3632)

This commit is contained in:
Alireza authored and GitHub committed 2023-09-22 10:23:44 -04:00
1 parent 896866e9d0
commit 48bbd6281a
186 files changed
+6801 -2883

No files matched your search

@@ -0,0 +1,23 @@
import React from 'react';
import Icon from '../Icon';
function AddSegmentRow({ onClick }) {
return (
<div
className="flex hover:cursor-pointer"
onClick={onClick}
>
<div className="h-[28px] w-[28px]"></div>
<div className="group ml-2.5 mt-1">
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pr-2">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segment</span>
</div>
</div>
</div>
);
}
export default AddSegmentRow;
@@ -0,0 +1,20 @@
import React from 'react';
import Icon from '../Icon';
function NoSegmentationRow({ onSegmentationAdd }) {
return (
<div
className="group"
onClick={onSegmentationAdd}
>
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] group-hover:cursor-pointer">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segmentation</span>
</div>
</div>
);
}
export default NoSegmentationRow;
@@ -1,10 +1,13 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import Icon from '../Icon';
import InputRange from '../InputRange';
import CheckBox from '../CheckBox';
import InputNumber from '../InputNumber';
import classNames from 'classnames';
const getRoundedValue = value => {
return Math.round(value * 100) / 100;
};
const ActiveSegmentationConfig = ({
config,
@@ -15,7 +18,7 @@ const ActiveSegmentationConfig = ({
setFillAlpha,
}) => {
return (
<div className="flex justify-between px-2 pt-[13px] text-[12px]">
<div className="flex justify-between px-3 pt-[13px] text-[12px]">
<div className="flex flex-col items-start">
<div className="mb-[12px] text-white">Active</div>
<CheckBox
@@ -39,7 +42,7 @@ const ActiveSegmentationConfig = ({
<InputRange
minValue={0}
maxValue={100}
value={config.outlineOpacity * 100}
value={getRoundedValue(config.outlineOpacity * 100)}
onChange={setOutlineOpacityActive}
step={1}
containerClassName="mt-[4px] mb-[9px]"
@@ -48,9 +51,9 @@ const ActiveSegmentationConfig = ({
unit="%"
/>
<InputRange
minValue={0}
minValue={4}
maxValue={100}
value={config.fillAlpha * 100}
value={getRoundedValue(config.fillAlpha * 100)}
onChange={setFillAlpha}
step={1}
containerClassName="mt-[4px] mb-[9px]"
@@ -80,11 +83,11 @@ const InactiveSegmentationConfig = ({
setFillAlphaInactive,
}) => {
return (
<div className="px-2">
<div className="px-3">
<CheckBox
label="Display Inactive Segmentations"
checked={config.renderInactiveSegmentations}
labelClassName="text-[12px] pt-1"
labelClassName="text-[12px]"
className="mb-[9px]"
onChange={setRenderInactiveSegmentations}
/>
@@ -94,7 +97,7 @@ const InactiveSegmentationConfig = ({
<InputRange
minValue={0}
maxValue={100}
value={config.fillAlphaInactive * 100}
value={getRoundedValue(config.fillAlphaInactive * 100)}
onChange={setFillAlphaInactive}
step={1}
containerClassName="mt-[4px]"
@@ -120,51 +123,37 @@ const SegmentationConfig = ({
const { initialConfig } = segmentationConfig;
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('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
<div className="bg-primary-dark select-none">
<div>
<ActiveSegmentationConfig
config={initialConfig}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
/>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Segmentation Appearance'}</span>
</div>
{/* active segmentation */}
{!isMinimized && (
<div>
<ActiveSegmentationConfig
config={initialConfig}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
<div className="mx-1 mb-[8px] h-[1px] bg-[#212456]"></div>
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex cursor-pointer items-center pl-2 pb-[9px]"
>
<Icon
name="panel-group-open-close"
className={classNames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
/>
{/* A small line */}
<div className="mx-1 mb-[8px] h-[1px] bg-[#212456]"></div>
{/* inactive segmentation */}
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex cursor-pointer items-center pl-2 pb-[9px]"
>
<span className="text-[12px] font-[300] text-[#d8d8d8]">
{'Inactive Segmentations'}
</span>
</div>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Inactive Segmentations'}</span>
</div>
{!isMinimized && (
<InactiveSegmentationConfig
config={initialConfig}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setFillAlphaInactive={setFillAlphaInactive}
/>
</div>
)}
)}
</div>
<div className="h-[6px] bg-black "></div>
</div>
);
@@ -0,0 +1,160 @@
import React from 'react';
import { Select, Icon, Dropdown } from '../../components';
import PropTypes from 'prop-types';
function SegmentationDropDownRow({
segmentations = [],
activeSegmentation,
onActiveSegmentationChange,
disableEditing,
onToggleSegmentationVisibility,
onSegmentationEdit,
onSegmentationDownload,
storeSegmentation,
onSegmentationDelete,
onSegmentationAdd,
}) {
const handleChange = option => {
onActiveSegmentationChange(option.value); // Notify the parent
};
const selectOptions = segmentations.map(s => ({
value: s.id,
label: s.label,
}));
if (!activeSegmentation) {
return null;
}
return (
<div className="group mx-0.5 mt-[8px] flex items-center">
<div
onClick={e => {
e.stopPropagation();
}}
>
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
alignment="left"
itemsClassName="text-primary-active"
showBorders={false}
list={[
...(!disableEditing
? [
{
title: 'Add New Segmentation',
onClick: () => {
onSegmentationAdd();
},
},
]
: []),
...(!disableEditing
? [
{
title: 'Rename',
onClick: () => {
onSegmentationEdit(activeSegmentation.id);
},
},
]
: []),
{
title: 'Delete',
onClick: () => {
onSegmentationDelete(activeSegmentation.id);
},
},
...(!disableEditing
? [
{
title: 'Export DICOM SEG',
onClick: () => {
storeSegmentation(activeSegmentation.id);
},
},
{
title: 'Download',
onClick: () => {
onSegmentationDownload(activeSegmentation.id);
},
},
]
: []),
]}
>
<div className="hover:bg-secondary-dark mx-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
</div>
{selectOptions?.length && (
<Select
id="segmentation-select"
isClearable={false}
onChange={handleChange}
components={{
DropdownIndicator: () => (
<Icon
name={'chevron-down-new'}
className="mr-2"
/>
),
}}
isSearchable={false}
options={selectOptions}
value={selectOptions?.find(o => o.value === activeSegmentation.id)}
className="text-aqua-pale h-[26px] w-1/2 text-[13px]"
/>
)}
<div className="flex items-center">
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
>
{activeSegmentation.isVisible ? (
<Icon
name="row-shown"
className="text-primary-active"
/>
) : (
<Icon
name="row-hidden"
className="text-primary-active"
/>
)}
</div>
</div>
</div>
);
}
SegmentationDropDownRow.propTypes = {
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
activeSegmentation: PropTypes.shape({
id: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
}),
onActiveSegmentationChange: PropTypes.func.isRequired,
disableEditing: PropTypes.bool,
onToggleSegmentationVisibility: PropTypes.func,
onSegmentationEdit: PropTypes.func,
onSegmentationDownload: PropTypes.func,
storeSegmentation: PropTypes.func,
onSegmentationDelete: PropTypes.func,
onSegmentationAdd: PropTypes.func,
};
SegmentationDropDownRow.defaultProps = {
segmentations: [],
disableEditing: false,
};
export default SegmentationDropDownRow;
@@ -1,287 +0,0 @@
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,
disableEditing,
}) => {
return (
<div>
<div className="text-primary-active flex cursor-pointer items-center bg-black py-1 pl-[29px] text-[12px] hover:opacity-80">
{showAddSegment && !disableEditing && (
<div
className="flex items-center"
onClick={() => onSegmentAdd()}
>
<Icon
name="row-add"
className="h-5 w-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="h-5 w-5"
/>
) : (
<Icon
name="row-hide-all"
className="h-5 w-5"
/>
)}
</div>
</div>
{/* {isSegmentationConfigOpen && (
<SegmentationConfig onConfigChange={onConfigChange} />
)} */}
</div>
);
};
const SegmentGroupHeader = ({
isMinimized,
onToggleMinimizeSegmentation,
onSegmentationClick,
id,
label,
isActive,
segmentCount,
onSegmentationEdit,
disableEditing,
onSegmentationDelete,
}) => {
return (
<div
className={classnames(
'flex-static border-secondary-light flex h-[27px] cursor-pointer items-center gap-2 rounded-t-md border-b pr-2 pl-[3px] 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('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
/>
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap text-white">
{label.toUpperCase()}
</span>
<div className="flex-grow" />
<span className="text-white ">{segmentCount}</span>
<div
onClick={e => {
e.stopPropagation();
}}
>
{!disableEditing && (
<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,
disableEditing,
showAddSegment,
segments,
activeSegmentIndex,
onSegmentAdd,
onSegmentationClick,
onClickSegmentColor,
onSegmentationEdit,
onToggleSegmentVisibility,
onToggleSegmentationVisibility,
onSegmentDelete,
showSegmentDelete,
onToggleMinimizeSegmentation,
onSegmentationConfigChange,
onSegmentationDelete,
onSegmentEdit,
}) => {
return (
<div className="flex min-h-0 flex-auto flex-col">
<SegmentGroupHeader
id={id}
label={label}
isActive={isActive}
isMinimized={isMinimized}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationClick={onSegmentationClick}
segmentCount={segmentCount}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
disableEditing={disableEditing}
/>
{!isMinimized && (
<div className="flex min-h-0 flex-auto flex-col">
<AddNewSegmentRow
onConfigChange={onSegmentationConfigChange}
onSegmentAdd={onSegmentAdd}
isVisible={isVisible}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
id={id}
disableEditing={disableEditing}
showAddSegment={showAddSegment}
/>
<div className="ohif-scrollbar flex min-h-0 flex-col overflow-y-hidden">
{!!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}
disableEditing={disableEditing}
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,
disableEditing: PropTypes.bool,
};
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;
@@ -11,7 +11,7 @@ const SegmentItem = ({
isActive,
isVisible,
color,
showSegmentDelete,
showDelete,
disableEditing,
isLocked = false,
onClick,
@@ -21,200 +21,168 @@ const SegmentItem = ({
onToggleVisibility,
onToggleLocked,
}) => {
const [isHovering, setIsHovering] = useState(false);
const [isSegmentIndexHovering, setIsSegmentIndexHovering] = useState(false);
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<div
className={classnames(
'bg-primary-dark group relative flex cursor-pointer overflow-hidden border text-[12px] transition duration-300',
{
'border-primary-light rounded-[3px]': isActive,
},
{
'border-transparent': !isActive,
}
)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={classnames('text-aqua-pale group/row flex min-h-[28px] bg-black')}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
}}
role="button"
tabIndex={0}
data-cy={'segment-item'}
>
<div
className={classnames(
'flex w-[27px] items-center justify-center border-r border-r-black text-[12px]',
{
'bg-primary-light border-l-primary-light rounded-l-sm border-l text-black': isActive,
'rounded-l-sm border-l border-l-[#1d3c58] bg-[#1d3c58]': !isActive && isHovering,
'bg-primary-dark text-aqua-pale': !isActive && !isHovering && isVisible,
'bg-[#140e2e] text-[#537594] opacity-60': !isActive && !isVisible,
}
)}
onMouseEnter={() => setIsSegmentIndexHovering(true)}
onMouseLeave={() => setIsSegmentIndexHovering(false)}
className={classnames('bg-primary-dark group/number grid w-[32px] place-items-center', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
'border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isSegmentIndexHovering && showSegmentDelete ? (
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('pr-0.5')}
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div className={classnames('flex items-center pr-2 ')}>{segmentIndex}</div>
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames(
'relative flex w-full items-center justify-between border-r border-r-black py-1 pl-2 text-white ',
{
'bg-primary-dark text-primary-light': isActive,
'bg-primary-dark text-aqua-pale': !isActive && isVisible,
'bg-[#140e2e] text-[#3d5871] opacity-60': !isVisible,
}
)}
className={classnames('relative flex w-full', {
'border-primary-light bg-primary-dark rounded-r-[4px] border border-l-0': isActive,
'border border-l-0 border-transparent': !isActive,
})}
>
<div className={classnames('flex items-baseline gap-2')}>
<div
className={classnames('h-[8px] w-[8px] shrink-0 rounded-full')}
style={{ backgroundColor: cssColor }}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
/>
<div className="">{label}</div>
</div>
{/* with faded background */}
<div className="bg-black/15 absolute right-0 rounded-lg pr-[7px]">
{!isVisible && !isHovering && (
<div>
<Icon
name="row-hidden"
className={classnames('h-5 w-5 text-[#3d5871] ')}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
)}
{isHovering && (
<div className={classnames('flex items-center')}>
{!disableEditing && (
<Icon
name="row-edit"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
)}
{isVisible ? (
<Icon
name="row-hide"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
) : (
<Icon
name="row-unhide"
className={classnames('h-5 w-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="group-hover/row:bg-primary-dark flex h-full w-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames(
'w-3 h-3 mt-1 mr-2 rounded-full cursor-pointer transition duration-300 hover:opacity-80'
)}
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
style={{ backgroundColor: cssColor }}
></div>
{label}
/>
</div>
<div className="flex items-center w-1/3">
<div className="px-1">
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
</div>
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isVisible ? 'eye-visible' : 'eye-hidden'}
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
{onToggleLocked !== undefined ? (
<div className="px-1">
)}
</div>
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isLocked ? 'lock' : 'dotted-circle'}
name="row-lock"
className="h-5 w-5 text-[#3d5871]"
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'
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
/>
)}
name={'pencil'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
</div>
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
</div>
</div>
</div> */}
</div>
</div>
);
};
const HoveringIcons = ({
onEdit,
isLocked,
isVisible,
onToggleLocked,
onToggleVisibility,
segmentationId,
segmentIndex,
}) => {
const iconClass = 'w-5 h-5 hover:cursor-pointer hover:opacity-60';
const handleIconClick = (e, action) => {
e.stopPropagation();
action(segmentationId, segmentIndex);
};
const createIcon = (name, action, color = null) => (
<Icon
name={name}
className={classnames(iconClass, color ?? 'text-white')}
onClick={e => handleIconClick(e, action)}
/>
);
return (
<div className="flex items-center">
{createIcon('row-edit', onEdit)}
{createIcon(
isLocked ? 'row-lock' : 'row-unlock',
onToggleLocked,
isLocked ? 'text-[#3d5871]' : null
)}
{createIcon(
isVisible ? 'row-shown' : 'row-hidden',
onToggleVisibility,
!isVisible ? 'text-[#3d5871]' : null
)}
</div>
);
};
@@ -1,28 +1,38 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import SegmentationGroup from './SegmentationGroup';
import { PanelSection } from '../../components';
import SegmentationConfig from './SegmentationConfig';
import SegmentationDropDownRow from './SegmentationDropDownRow';
import NoSegmentationRow from './NoSegmentationRow';
import AddSegmentRow from './AddSegmentRow';
import SegmentationGroupSegment from './SegmentationGroupSegment';
const SegmentationGroupTable = ({
segmentations,
// segmentation initial config
segmentationConfig,
// UI show/hide
disableEditing,
showAddSegmentation,
showAddSegment,
showDeleteSegment,
// segmentation/segment handlers
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
showAddSegmentation,
showAddSegment,
onSegmentationDownload,
storeSegmentation,
// segment handlers
onSegmentClick,
onSegmentAdd,
segmentationConfig,
disableEditing,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentColorClick,
isMinimized,
onToggleMinimizeSegmentation,
// segmentation config handlers
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
@@ -31,111 +41,191 @@ const SegmentationGroupTable = ({
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
const onActiveSegmentationChange = segmentationId => {
onSegmentationClick(segmentationId);
setActiveSegmentationId(segmentationId);
};
useEffect(() => {
// find the first active segmentation to set
let activeSegmentationIdToSet = segmentations?.find(segmentation => segmentation.isActive)?.id;
// If there is no active segmentation, set the first one to be active
if (!activeSegmentationIdToSet && segmentations?.length > 0) {
activeSegmentationIdToSet = segmentations[0].id;
}
// If there is no segmentation, set the active segmentation to null
if (segmentations?.length === 0) {
activeSegmentationIdToSet = null;
}
setActiveSegmentationId(activeSegmentationIdToSet);
}, [segmentations]);
const activeSegmentation = segmentations?.find(
segmentation => segmentation.id === activeSegmentationId
);
return (
<div className="font-inter flex min-h-0 flex-col font-[300]">
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
<div className="mt-1 flex min-h-0 flex-col pr-[1px]">
{!!segmentations.length &&
segmentations.map((segmentation, index) => {
const {
id,
label,
displayText = [],
segmentCount,
segments,
isVisible,
isActive,
activeSegmentIndex,
} = segmentation;
return (
<SegmentationGroup
id={id}
key={id}
label={label}
disableEditing={disableEditing}
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>
{showAddSegmentation && !disableEditing && (
<div
className="text-primary-active flex h-[45px] cursor-pointer items-center bg-black pl-1 text-[12px] hover:opacity-80"
onClick={() => onSegmentationAdd()}
>
<Icon
name="row-add"
className="h-5 w-5"
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<PanelSection
title="Segmentation"
actionIcons={
activeSegmentation && [
{
name: 'settings-bars',
onClick: () => setIsConfigOpen(isOpen => !isOpen),
},
]
}
>
{isConfigOpen && (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
<div className="pl-1">Add New Segmentation</div>
)}
<div className="bg-primary-dark">
{segmentations?.length === 0 ? (
<div className="select-none rounded-[4px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
</div>
) : (
<div className="mt-1 select-none">
<SegmentationDropDownRow
segmentations={segmentations}
disableEditing={disableEditing}
activeSegmentation={activeSegmentation}
onActiveSegmentationChange={onActiveSegmentationChange}
onSegmentationDelete={onSegmentationDelete}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDownload={onSegmentationDownload}
storeSegmentation={storeSegmentation}
onSegmentationAdd={onSegmentationAdd}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
/>
{!disableEditing && showAddSegment && (
<AddSegmentRow onClick={() => onSegmentAdd(activeSegmentationId)} />
)}
</div>
)}
</div>
)}
{activeSegmentation && (
<div className="ohif-scrollbar mt-1.5 flex min-h-0 flex-col overflow-y-hidden">
{activeSegmentation?.segments?.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked } = segment;
return (
<div
className="mb-[1px]"
key={segmentIndex}
>
<SegmentationGroupSegment
segmentationId={activeSegmentationId}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={activeSegmentation.activeSegmentIndex === segmentIndex}
disableEditing={disableEditing}
isLocked={isLocked}
isVisible={isVisible}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showDelete={showDeleteSegment}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
onToggleLocked={onToggleSegmentLock}
/>
</div>
);
})}
</div>
)}
</PanelSection>
</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,
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
segments: PropTypes.arrayOf(
PropTypes.shape({
segmentIndex: PropTypes.number.isRequired,
color: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
})
),
})
),
segmentationConfig: PropTypes.object.isRequired,
disableEditing: PropTypes.bool,
showAddSegmentation: PropTypes.bool,
showAddSegment: PropTypes.bool,
showDeleteSegment: PropTypes.bool,
onSegmentationAdd: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentationDownload: PropTypes.func.isRequired,
storeSegmentation: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentLock: PropTypes.func.isRequired,
onSegmentColorClick: PropTypes.func.isRequired,
setFillAlpha: PropTypes.func.isRequired,
setFillAlphaInactive: PropTypes.func.isRequired,
setOutlineWidthActive: PropTypes.func.isRequired,
setOutlineOpacityActive: PropTypes.func.isRequired,
setRenderFill: PropTypes.func.isRequired,
setRenderInactiveSegmentations: PropTypes.func.isRequired,
setRenderOutline: PropTypes.func.isRequired,
};
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,
},
},
disableEditing: false,
showAddSegmentation: true,
showAddSegment: true,
showDeleteSegment: true,
onSegmentationAdd: () => {},
onSegmentationEdit: () => {},
onSegmentationClick: () => {},
onSegmentationDelete: () => {},
onSegmentationDownload: () => {},
storeSegmentation: () => {},
onSegmentClick: () => {},
onSegmentAdd: () => {},
onSegmentDelete: () => {},
onSegmentEdit: () => {},
onToggleSegmentationVisibility: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentLock: () => {},
onSegmentColorClick: () => {},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
@@ -144,5 +234,4 @@ SegmentationGroupTable.defaultProps = {
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTable;
@@ -1,7 +1,5 @@
import SegmentationGroupTable from '../SegmentationGroupTable';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
import { SegmentationGroupTable } from '../../../components';
export const argTypes = {
component: SegmentationGroupTable,
@@ -40,13 +38,14 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
showAddSegmentation: true,
isMinimized: { '4be5-b9b2-ed33-fa86970568e7': false },
showAddSegment: true,
showDeleteSegment: true,
segmentations: [
{
id: '4be5-b9b2-ed33-fa86970568e7',
volumeId: 'a3b0f15f-4be5-b9b2-ed33-fa86970568e7',
activeSegmentIndex: 1,
activeSegmentIndex: 2,
cachedStats: {},
label: 'Segmentation',
label: 'Segmentation 1',
segmentsLocked: {},
type: 'LABELMAP',
displayText: [],
@@ -55,7 +54,7 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
segments: [
null,
{
label: 'Segment 1',
label: 'This is a very long segment label',
segmentIndex: 1,
color: [128, 174, 128],
opacity: 255,
@@ -88,7 +87,7 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
volumeId: 'a3b0f15f-4be5-b9b2-ed33-fa86970568e7',
activeSegmentIndex: 1,
cachedStats: {},
label: 'Segmentation2',
label: '2 Segmentation',
segmentsLocked: {},
type: 'LABELMAP',
displayText: [],