feat(segmentation): Enhanced segmentation panel design for TMTV (#3988)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
This commit is contained in:
parent
e8e209291c
commit
9f3235ff09
@ -206,6 +206,9 @@ const commandsModule = ({
|
|||||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
||||||
// Todo: handle adding more than one segmentation
|
// Todo: handle adding more than one segmentation
|
||||||
const displaySet = displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
displaySet.referencedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
updateViewportsForSegmentationRendering({
|
updateViewportsForSegmentationRendering({
|
||||||
viewportId,
|
viewportId,
|
||||||
@ -221,7 +224,8 @@ const commandsModule = ({
|
|||||||
|
|
||||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
||||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
|
||||||
return segmentationId;
|
return segmentationId;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import { Toolbox } from '@ohif/ui';
|
import { Toolbox } from '@ohif/ui';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
|
import { SegmentationPanelMode } from './types/segmentation';
|
||||||
|
|
||||||
const getPanelModule = ({
|
const getPanelModule = ({
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -17,6 +18,9 @@ const getPanelModule = ({
|
|||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
||||||
|
const segmentationPanelMode =
|
||||||
|
customizationService.get('segmentation.segmentationPanelMode')?.value ||
|
||||||
|
SegmentationPanelMode.Dropdown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
@ -26,12 +30,18 @@ const getPanelModule = ({
|
|||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
|
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
|
||||||
|
segmentationPanelMode: segmentationPanelMode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappedPanelSegmentationWithTools = configuration => {
|
const wrappedPanelSegmentationWithTools = configuration => {
|
||||||
|
const [appConfig] = useAppConfig();
|
||||||
|
const segmentationPanelMode =
|
||||||
|
customizationService.get('segmentation.segmentationPanelMode')?.value ||
|
||||||
|
SegmentationPanelMode.Dropdown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbox
|
<Toolbox
|
||||||
@ -50,6 +60,7 @@ const getPanelModule = ({
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
|
segmentationPanelMode: segmentationPanelMode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'evaluate.cornerstone.segmentation',
|
name: 'evaluate.cornerstone.segmentation',
|
||||||
evaluate: ({ viewportId, button, toolNames }) => {
|
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||||
// Todo: we need to pass in the button section Id since we are kind of
|
// Todo: we need to pass in the button section Id since we are kind of
|
||||||
// forcing the button to have black background since initially
|
// forcing the button to have black background since initially
|
||||||
// it is designed for the toolbox not the toolbar on top
|
// it is designed for the toolbox not the toolbar on top
|
||||||
@ -13,6 +13,7 @@ export function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright !bg-black opacity-50',
|
className: '!text-common-bright !bg-black opacity-50',
|
||||||
|
disabledText: disabledText ?? 'No segmentations available',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright ohif-disabled',
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import { createReportAsync } from '@ohif/extension-default';
|
import { createReportAsync } from '@ohif/extension-default';
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SegmentationGroupTable } from '@ohif/ui';
|
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
|
||||||
|
import { SegmentationPanelMode } from '../types/segmentation';
|
||||||
import callInputDialog from './callInputDialog';
|
import callInputDialog from './callInputDialog';
|
||||||
import callColorPickerDialog from './colorPickerDialog';
|
import callColorPickerDialog from './colorPickerDialog';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
|
||||||
|
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
|
||||||
|
};
|
||||||
|
|
||||||
export default function PanelSegmentation({
|
export default function PanelSegmentation({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -170,6 +175,22 @@ export default function PanelSegmentation({
|
|||||||
|
|
||||||
const onToggleSegmentationVisibility = segmentationId => {
|
const onToggleSegmentationVisibility = segmentationId => {
|
||||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
const isVisible = segmentation.isVisible;
|
||||||
|
const segments = segmentation.segments;
|
||||||
|
|
||||||
|
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||||
|
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
segments.forEach((segment, segmentIndex) => {
|
||||||
|
segmentationService.setSegmentVisibility(
|
||||||
|
segmentationId,
|
||||||
|
segmentIndex,
|
||||||
|
isVisible,
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const _setSegmentationConfiguration = useCallback(
|
const _setSegmentationConfiguration = useCallback(
|
||||||
@ -221,10 +242,10 @@ export default function PanelSegmentation({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SegmentationGroupTableComponent = components[configuration?.segmentationPanelMode];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SegmentationGroupTableComponent
|
||||||
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
|
||||||
<SegmentationGroupTable
|
|
||||||
title={t('Segmentations')}
|
title={t('Segmentations')}
|
||||||
segmentations={segmentations}
|
segmentations={segmentations}
|
||||||
disableEditing={configuration.disableEditing}
|
disableEditing={configuration.disableEditing}
|
||||||
@ -256,11 +277,7 @@ export default function PanelSegmentation({
|
|||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||||
}
|
}
|
||||||
setRenderInactiveSegmentations={value =>
|
setRenderInactiveSegmentations={value =>
|
||||||
_setSegmentationConfiguration(
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
|
||||||
selectedSegmentationId,
|
|
||||||
'renderInactiveSegmentations',
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
setOutlineWidthActive={value =>
|
setOutlineWidthActive={value =>
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||||
@ -272,8 +289,6 @@ export default function PanelSegmentation({
|
|||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export enum SegmentationPanelMode {
|
||||||
|
Expanded = 'expanded',
|
||||||
|
Dropdown = 'dropdown',
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
// enabled or not
|
// enabled or not
|
||||||
{
|
{
|
||||||
name: 'evaluate.cornerstoneTool',
|
name: 'evaluate.cornerstoneTool',
|
||||||
evaluate: ({ viewportId, button }) => {
|
evaluate: ({ viewportId, button, disabledText }) => {
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||||
|
|
||||||
if (!toolGroup) {
|
if (!toolGroup) {
|
||||||
@ -34,6 +34,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright ohif-disabled',
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'evaluate.cornerstoneTool.toggle',
|
name: 'evaluate.cornerstoneTool.toggle',
|
||||||
evaluate: ({ viewportId, button }) => {
|
evaluate: ({ viewportId, button, disabledText }) => {
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||||
|
|
||||||
if (!toolGroup) {
|
if (!toolGroup) {
|
||||||
@ -121,6 +122,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright ohif-disabled',
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,13 +170,14 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'evaluate.not3D',
|
name: 'evaluate.not3D',
|
||||||
evaluate: ({ viewportId, button }) => {
|
evaluate: ({ viewportId, disabledText }) => {
|
||||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
if (viewport?.type === 'volume3d') {
|
if (viewport?.type === 'volume3d') {
|
||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright ohif-disabled',
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -211,7 +214,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'evaluate.mpr',
|
name: 'evaluate.mpr',
|
||||||
evaluate: ({ viewportId, button }) => {
|
evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => {
|
||||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
|
|
||||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||||
@ -230,6 +233,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright ohif-disabled',
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -988,6 +988,7 @@ class SegmentationService extends PubSubService {
|
|||||||
referencedVolumeId: volumeId, // Todo: this is so ugly
|
referencedVolumeId: volumeId, // Todo: this is so ugly
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
description: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.addOrUpdateSegmentation(segmentation);
|
this.addOrUpdateSegmentation(segmentation);
|
||||||
|
|||||||
@ -20,6 +20,8 @@ type Segment = {
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
// whether the segment is locked
|
// whether the segment is locked
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
|
// display texts
|
||||||
|
displayText?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Segmentation = {
|
type Segmentation = {
|
||||||
|
|||||||
@ -21,8 +21,6 @@ export function Toolbar({ servicesManager }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { id, Component, componentProps } = toolDef;
|
const { id, Component, componentProps } = toolDef;
|
||||||
const { disabled } = componentProps;
|
|
||||||
|
|
||||||
const tool = (
|
const tool = (
|
||||||
<Component
|
<Component
|
||||||
key={id}
|
key={id}
|
||||||
@ -33,16 +31,7 @@ export function Toolbar({ servicesManager }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return disabled ? (
|
return (
|
||||||
<Tooltip
|
|
||||||
key={id}
|
|
||||||
position="bottom"
|
|
||||||
content={componentProps.label}
|
|
||||||
secondaryContent={'Not available on the current viewport'}
|
|
||||||
>
|
|
||||||
<div className={classnames('mr-1')}>{tool}</div>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
|
|||||||
@ -7,11 +7,14 @@ const pkg = require('./../package.json');
|
|||||||
const ROOT_DIR = path.join(__dirname, './..');
|
const ROOT_DIR = path.join(__dirname, './..');
|
||||||
const SRC_DIR = path.join(__dirname, '../src');
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
const DIST_DIR = path.join(__dirname, '../dist');
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
const ENTRY = {
|
const ENTRY = {
|
||||||
app: `${SRC_DIR}/index.tsx`,
|
app: `${SRC_DIR}/index.tsx`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||||
|
|
||||||
module.exports = (env, argv) => {
|
module.exports = (env, argv) => {
|
||||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||||
|
|
||||||
@ -42,6 +45,10 @@ module.exports = (env, argv) => {
|
|||||||
new webpack.optimize.LimitChunkCountPlugin({
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
maxChunks: 1,
|
maxChunks: 1,
|
||||||
}),
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: `./dist/${outputName}.css`,
|
||||||
|
chunkFilename: `./dist/${outputName}.css`,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Input, Button } from '@ohif/ui';
|
import { PanelSection, Input, Button } from '@ohif/ui';
|
||||||
import { DicomMetadataStore, ServicesManager } from '@ohif/core';
|
import { DicomMetadataStore, ServicesManager } from '@ohif/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -126,14 +126,16 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
||||||
{
|
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
|
||||||
|
<PanelSection title={t('Patient Information')}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="bg-primary-dark flex flex-col space-y-4 p-4">
|
<div className="bg-primary-dark flex flex-col gap-4 p-2">
|
||||||
<Input
|
<Input
|
||||||
|
containerClassName={'!flex-row !justify-between items-center'}
|
||||||
label={t('Patient Sex')}
|
label={t('Patient Sex')}
|
||||||
labelClassName="text-white mb-2"
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
className="mt-1"
|
className="!m-0 !h-[26px] !w-[117px]"
|
||||||
value={metadata.PatientSex || ''}
|
value={metadata.PatientSex || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
handleMetadataChange({
|
handleMetadataChange({
|
||||||
@ -142,9 +144,11 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('Patient Weight (kg)')}
|
containerClassName={'!flex-row !justify-between items-center'}
|
||||||
labelClassName="text-white mb-2"
|
label={t('Weight')}
|
||||||
className="mt-1"
|
labelChildren={<span className="text-aqua-pale"> kg</span>}
|
||||||
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
|
className="!m-0 !h-[26px] !w-[117px]"
|
||||||
value={metadata.PatientWeight || ''}
|
value={metadata.PatientWeight || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
handleMetadataChange({
|
handleMetadataChange({
|
||||||
@ -153,9 +157,11 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('Total Dose (bq)')}
|
containerClassName={'!flex-row !justify-between items-center'}
|
||||||
labelClassName="text-white mb-2"
|
label={t('Total Dose')}
|
||||||
className="mt-1"
|
labelChildren={<span className="text-aqua-pale"> bq</span>}
|
||||||
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
|
className="!m-0 !h-[26px] !w-[117px]"
|
||||||
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''}
|
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
handleMetadataChange({
|
handleMetadataChange({
|
||||||
@ -166,9 +172,11 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('Half Life (s)')}
|
containerClassName={'!flex-row !justify-between items-center'}
|
||||||
labelClassName="text-white mb-2"
|
label={t('Half Life')}
|
||||||
className="mt-1"
|
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||||
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
|
className="!m-0 !h-[26px] !w-[117px]"
|
||||||
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''}
|
value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
handleMetadataChange({
|
handleMetadataChange({
|
||||||
@ -179,9 +187,11 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('Injection Time (s)')}
|
containerClassName={'!flex-row !justify-between items-center'}
|
||||||
labelClassName="text-white mb-2"
|
label={t('Injection Time')}
|
||||||
className="mt-1"
|
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||||
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
|
className="!m-0 !h-[26px] !w-[117px]"
|
||||||
value={
|
value={
|
||||||
metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || ''
|
metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || ''
|
||||||
}
|
}
|
||||||
@ -194,16 +204,24 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('Acquisition Time (s)')}
|
containerClassName={'!flex-row !justify-between items-center'}
|
||||||
labelClassName="text-white mb-2"
|
label={t('Acquisition Time')}
|
||||||
className="mt-1 mb-2"
|
labelChildren={<span className="text-aqua-pale"> s</span>}
|
||||||
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
|
className="!m-0 !h-[26px] !w-[117px]"
|
||||||
value={metadata.SeriesTime || ''}
|
value={metadata.SeriesTime || ''}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={updateMetadata}>Reload Data</Button>
|
<Button
|
||||||
|
className="!h-[26px] !w-[115px] self-end !p-0"
|
||||||
|
onClick={updateMetadata}
|
||||||
|
>
|
||||||
|
Reload Data
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</PanelSection>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,287 @@
|
|||||||
|
import React, { useEffect, useState, useCallback, useReducer } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { SegmentationTable, Button, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import segmentationEditHandler from './segmentationEditHandler';
|
||||||
|
import ExportReports from './ExportReports';
|
||||||
|
import ROIThresholdConfiguration, { ROI_STAT } from './ROIThresholdConfiguration';
|
||||||
|
|
||||||
|
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
||||||
|
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
||||||
|
const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
|
||||||
|
const UPPER_PT_THRESHOLD_DEFAULT = 100;
|
||||||
|
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
|
||||||
|
const DEFAULT_STRATEGY = ROI_STAT;
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
const { payload } = action;
|
||||||
|
const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'setStrategy':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
strategy,
|
||||||
|
};
|
||||||
|
case 'setThreshold':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
ctLower: ctLower ? ctLower : state.ctLower,
|
||||||
|
ctUpper: ctUpper ? ctUpper : state.ctUpper,
|
||||||
|
ptLower: ptLower ? ptLower : state.ptLower,
|
||||||
|
ptUpper: ptUpper ? ptUpper : state.ptUpper,
|
||||||
|
};
|
||||||
|
case 'setWeight':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
weight,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LegacyPanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
|
||||||
|
const { t } = useTranslation('PanelSUV');
|
||||||
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
|
const [labelmapLoading, setLabelmapLoading] = useState(false);
|
||||||
|
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||||
|
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||||
|
|
||||||
|
const [config, dispatch] = useReducer(reducer, {
|
||||||
|
strategy: DEFAULT_STRATEGY,
|
||||||
|
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
|
||||||
|
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
|
||||||
|
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
|
||||||
|
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
|
||||||
|
weight: WEIGHT_DEFAULT,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [tmtvValue, setTmtvValue] = useState(null);
|
||||||
|
|
||||||
|
const runCommand = useCallback(
|
||||||
|
(commandName, commandOptions = {}) => {
|
||||||
|
return commandsManager.runCommand(commandName, commandOptions);
|
||||||
|
},
|
||||||
|
[commandsManager]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTMTVCalculation = useCallback(() => {
|
||||||
|
const tmtv = runCommand('calculateTMTV', { segmentations });
|
||||||
|
|
||||||
|
if (tmtv !== undefined) {
|
||||||
|
setTmtvValue(tmtv.toFixed(2));
|
||||||
|
}
|
||||||
|
}, [segmentations, runCommand]);
|
||||||
|
|
||||||
|
const handleROIThresholding = useCallback(() => {
|
||||||
|
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
||||||
|
segmentationId: selectedSegmentationId,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lesionStats = runCommand('getLesionStats', { labelmap });
|
||||||
|
const suvPeak = runCommand('calculateSuvPeak', { labelmap });
|
||||||
|
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||||
|
|
||||||
|
// update segDetails with the suv peak for the active segmentation
|
||||||
|
const segmentation = segmentationService.getSegmentation(selectedSegmentationId);
|
||||||
|
|
||||||
|
const cachedStats = {
|
||||||
|
lesionStats,
|
||||||
|
suvPeak,
|
||||||
|
lesionGlyoclysisStats,
|
||||||
|
};
|
||||||
|
|
||||||
|
const notYetUpdatedAtSource = true;
|
||||||
|
segmentationService.addOrUpdateSegmentation(
|
||||||
|
{
|
||||||
|
...segmentation,
|
||||||
|
...Object.assign(segmentation.cachedStats, cachedStats),
|
||||||
|
displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
|
||||||
|
},
|
||||||
|
notYetUpdatedAtSource
|
||||||
|
);
|
||||||
|
|
||||||
|
handleTMTVCalculation();
|
||||||
|
}, [selectedSegmentationId, config]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI based on segmentation changes (added, removed, updated)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
// ~~ Subscription
|
||||||
|
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||||
|
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||||
|
const subscriptions = [];
|
||||||
|
|
||||||
|
[added, updated].forEach(evt => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
setSegmentations(segmentations);
|
||||||
|
});
|
||||||
|
subscriptions.push(unsubscribe);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(unsub => {
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||||
|
() => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
setSegmentations(segmentations);
|
||||||
|
|
||||||
|
if (segmentations.length > 0) {
|
||||||
|
setSelectedSegmentationId(segmentations[0].id);
|
||||||
|
handleTMTVCalculation();
|
||||||
|
} else {
|
||||||
|
setSelectedSegmentationId(null);
|
||||||
|
setTmtvValue(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever the segmentations change, update the TMTV calculations
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedSegmentationId && segmentations.length > 0) {
|
||||||
|
setSelectedSegmentationId(segmentations[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTMTVCalculation();
|
||||||
|
}, [segmentations, selectedSegmentationId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||||
|
<div className="mx-4 my-4 mb-4 flex space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setLabelmapLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
||||||
|
setLabelmapLoading(false);
|
||||||
|
setSelectedSegmentationId(segmentationId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelmapLoading ? 'loading ...' : 'New Label'}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleROIThresholding}>Run</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="bg-secondary-dark border-secondary-light mb-2 flex h-8 cursor-pointer select-none items-center justify-around border-t outline-none first:border-0"
|
||||||
|
onClick={() => {
|
||||||
|
setShowConfig(!showConfig);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="px-4 text-base text-white">{t('ROI Threshold Configuration')}</div>
|
||||||
|
</div>
|
||||||
|
{showConfig && (
|
||||||
|
<ROIThresholdConfiguration
|
||||||
|
config={config}
|
||||||
|
dispatch={dispatch}
|
||||||
|
runCommand={runCommand}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* show segmentation table */}
|
||||||
|
<div className="mt-4">
|
||||||
|
{segmentations?.length ? (
|
||||||
|
<SegmentationTable
|
||||||
|
title={t('Segmentations')}
|
||||||
|
segmentations={segmentations}
|
||||||
|
activeSegmentationId={selectedSegmentationId}
|
||||||
|
onClick={id => {
|
||||||
|
runCommand('setSegmentationActiveForToolGroups', {
|
||||||
|
segmentationId: id,
|
||||||
|
});
|
||||||
|
setSelectedSegmentationId(id);
|
||||||
|
}}
|
||||||
|
onToggleVisibility={id => {
|
||||||
|
segmentationService.toggleSegmentationVisibility(id);
|
||||||
|
}}
|
||||||
|
onToggleVisibilityAll={ids => {
|
||||||
|
ids.map(id => {
|
||||||
|
segmentationService.toggleSegmentationVisibility(id);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDelete={id => {
|
||||||
|
segmentationService.remove(id);
|
||||||
|
}}
|
||||||
|
onEdit={id => {
|
||||||
|
segmentationEditHandler({
|
||||||
|
id,
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{tmtvValue !== null ? (
|
||||||
|
<div className="bg-secondary-dark mt-4 flex items-baseline justify-between px-2 py-1">
|
||||||
|
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||||
|
{'TMTV:'}
|
||||||
|
</span>
|
||||||
|
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<ExportReports
|
||||||
|
segmentations={segmentations}
|
||||||
|
tmtvValue={tmtvValue}
|
||||||
|
config={config}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mt-auto mb-4 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||||
|
onClick={() => {
|
||||||
|
// navigate to a url in a new tab
|
||||||
|
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
width="15px"
|
||||||
|
height="15px"
|
||||||
|
name={'info'}
|
||||||
|
className={'text-primary-active ml-4 mr-3'}
|
||||||
|
/>
|
||||||
|
<span>{'User Guide'}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyPanelRoiThresholdSegmentation.propTypes = {
|
||||||
|
commandsManager: PropTypes.shape({
|
||||||
|
runCommand: PropTypes.func.isRequired,
|
||||||
|
}),
|
||||||
|
servicesManager: PropTypes.shape({
|
||||||
|
services: PropTypes.shape({
|
||||||
|
segmentationService: PropTypes.shape({
|
||||||
|
getSegmentation: PropTypes.func.isRequired,
|
||||||
|
getSegmentations: PropTypes.func.isRequired,
|
||||||
|
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||||
|
subscribe: PropTypes.func.isRequired,
|
||||||
|
EVENTS: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@ -1,67 +1,23 @@
|
|||||||
import React, { useEffect, useState, useCallback, useReducer } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SegmentationTable, Button, Icon } from '@ohif/ui';
|
import { SegmentationGroupTableExpanded, Icon } from '@ohif/ui';
|
||||||
|
import { createReportAsync } from '@ohif/extension-default';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import segmentationEditHandler from './segmentationEditHandler';
|
import segmentationEditHandler from './segmentationEditHandler';
|
||||||
import ExportReports from './ExportReports';
|
import ExportReports from './ExportReports';
|
||||||
import ROIThresholdConfiguration, { ROI_STAT } from './ROIThresholdConfiguration';
|
import callInputDialog from './callInputDialog';
|
||||||
|
import callColorPickerDialog from './colorPickerDialog';
|
||||||
|
|
||||||
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
export default function PanelRoiThresholdSegmentation({
|
||||||
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
servicesManager,
|
||||||
const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
|
commandsManager,
|
||||||
const UPPER_PT_THRESHOLD_DEFAULT = 100;
|
extensionManager,
|
||||||
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
|
}) {
|
||||||
const DEFAULT_STRATEGY = ROI_STAT;
|
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
||||||
|
|
||||||
function reducer(state, action) {
|
|
||||||
const { payload } = action;
|
|
||||||
const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case 'setStrategy':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
strategy,
|
|
||||||
};
|
|
||||||
case 'setThreshold':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ctLower: ctLower ? ctLower : state.ctLower,
|
|
||||||
ctUpper: ctUpper ? ctUpper : state.ctUpper,
|
|
||||||
ptLower: ptLower ? ptLower : state.ptLower,
|
|
||||||
ptUpper: ptUpper ? ptUpper : state.ptUpper,
|
|
||||||
};
|
|
||||||
case 'setWeight':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
weight,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
|
|
||||||
const { segmentationService } = servicesManager.services;
|
|
||||||
|
|
||||||
const { t } = useTranslation('PanelSUV');
|
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
|
||||||
const [labelmapLoading, setLabelmapLoading] = useState(false);
|
|
||||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||||
|
|
||||||
const [config, dispatch] = useReducer(reducer, {
|
|
||||||
strategy: DEFAULT_STRATEGY,
|
|
||||||
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
|
|
||||||
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
|
|
||||||
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
|
|
||||||
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
|
|
||||||
weight: WEIGHT_DEFAULT,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [tmtvValue, setTmtvValue] = useState(null);
|
|
||||||
|
|
||||||
const runCommand = useCallback(
|
const runCommand = useCallback(
|
||||||
(commandName, commandOptions = {}) => {
|
(commandName, commandOptions = {}) => {
|
||||||
return commandsManager.runCommand(commandName, commandOptions);
|
return commandsManager.runCommand(commandName, commandOptions);
|
||||||
@ -69,46 +25,6 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
|||||||
[commandsManager]
|
[commandsManager]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTMTVCalculation = useCallback(() => {
|
|
||||||
const tmtv = runCommand('calculateTMTV', { segmentations });
|
|
||||||
|
|
||||||
if (tmtv !== undefined) {
|
|
||||||
setTmtvValue(tmtv.toFixed(2));
|
|
||||||
}
|
|
||||||
}, [segmentations, runCommand]);
|
|
||||||
|
|
||||||
const handleROIThresholding = useCallback(() => {
|
|
||||||
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
|
||||||
segmentationId: selectedSegmentationId,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const lesionStats = runCommand('getLesionStats', { labelmap });
|
|
||||||
const suvPeak = runCommand('calculateSuvPeak', { labelmap });
|
|
||||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
|
||||||
|
|
||||||
// update segDetails with the suv peak for the active segmentation
|
|
||||||
const segmentation = segmentationService.getSegmentation(selectedSegmentationId);
|
|
||||||
|
|
||||||
const cachedStats = {
|
|
||||||
lesionStats,
|
|
||||||
suvPeak,
|
|
||||||
lesionGlyoclysisStats,
|
|
||||||
};
|
|
||||||
|
|
||||||
const notYetUpdatedAtSource = true;
|
|
||||||
segmentationService.addOrUpdateSegmentation(
|
|
||||||
{
|
|
||||||
...segmentation,
|
|
||||||
...Object.assign(segmentation.cachedStats, cachedStats),
|
|
||||||
displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
|
|
||||||
},
|
|
||||||
notYetUpdatedAtSource
|
|
||||||
);
|
|
||||||
|
|
||||||
handleTMTVCalculation();
|
|
||||||
}, [selectedSegmentationId, config]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update UI based on segmentation changes (added, removed, updated)
|
* Update UI based on segmentation changes (added, removed, updated)
|
||||||
*/
|
*/
|
||||||
@ -116,9 +32,10 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
|||||||
// ~~ Subscription
|
// ~~ Subscription
|
||||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||||
|
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||||
const subscriptions = [];
|
const subscriptions = [];
|
||||||
|
|
||||||
[added, updated].forEach(evt => {
|
[added, updated, removed].forEach(evt => {
|
||||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||||
const segmentations = segmentationService.getSegmentations();
|
const segmentations = segmentationService.getSegmentations();
|
||||||
setSegmentations(segmentations);
|
setSegmentations(segmentations);
|
||||||
@ -133,109 +50,222 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const onSegmentationClick = (segmentationId: string) => {
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
||||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
setSelectedSegmentationId(segmentationId);
|
||||||
() => {
|
};
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
setSegmentations(segmentations);
|
|
||||||
|
|
||||||
if (segmentations.length > 0) {
|
const onSegmentationAdd = async () => {
|
||||||
setSelectedSegmentationId(segmentations[0].id);
|
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
||||||
handleTMTVCalculation();
|
setSelectedSegmentationId(segmentationId);
|
||||||
} else {
|
});
|
||||||
setSelectedSegmentationId(null);
|
};
|
||||||
setTmtvValue(null);
|
|
||||||
}
|
const onSegmentAdd = segmentationId => {
|
||||||
}
|
segmentationService.addSegment(segmentationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToolGroupIds = segmentationId => {
|
||||||
|
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||||
|
|
||||||
|
return toolGroupIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSegmentClick = (segmentationId, segmentIndex) => {
|
||||||
|
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
||||||
|
|
||||||
|
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||||
|
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
// const toolGroupId =
|
||||||
|
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||||
|
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _setSegmentationConfiguration = useCallback(
|
||||||
|
(segmentationId, key, value) => {
|
||||||
|
segmentationService.setConfiguration({
|
||||||
|
segmentationId,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[segmentationService]
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||||
unsubscribe();
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
const segmentInfo = segmentation.segments[segmentIndex];
|
||||||
|
const isVisible = !segmentInfo.isVisible;
|
||||||
|
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
segmentationService.setSegmentVisibility(
|
||||||
|
segmentationId,
|
||||||
|
segmentIndex,
|
||||||
|
isVisible,
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
||||||
* Whenever the segmentations change, update the TMTV calculations
|
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||||
*/
|
};
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedSegmentationId && segmentations.length > 0) {
|
const onSegmentEdit = (segmentationId, segmentIndex) => {
|
||||||
setSelectedSegmentationId(segmentations[0].id);
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
|
const segment = segmentation.segments[segmentIndex];
|
||||||
|
const { label } = segment;
|
||||||
|
|
||||||
|
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||||
|
if (label === '') {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTMTVCalculation();
|
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||||
}, [segmentations, selectedSegmentationId]);
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
||||||
|
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
|
const segment = segmentation.segments[segmentIndex];
|
||||||
|
const { color, opacity } = segment;
|
||||||
|
|
||||||
|
const rgbaColor = {
|
||||||
|
r: color[0],
|
||||||
|
g: color[1],
|
||||||
|
b: color[2],
|
||||||
|
a: opacity / 255.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||||
|
if (actionId === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
|
||||||
|
newRgbaColor.r,
|
||||||
|
newRgbaColor.g,
|
||||||
|
newRgbaColor.b,
|
||||||
|
newRgbaColor.a * 255.0,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeSegmentation = async segmentationId => {
|
||||||
|
const datasources = extensionManager.getActiveDataSource();
|
||||||
|
|
||||||
|
const displaySetInstanceUIDs = await createReportAsync({
|
||||||
|
servicesManager,
|
||||||
|
getReport: () =>
|
||||||
|
commandsManager.runCommand('storeSegmentation', {
|
||||||
|
segmentationId,
|
||||||
|
dataSource: datasources[0],
|
||||||
|
}),
|
||||||
|
reportType: 'Segmentation',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the exported report in the active viewport as read only (similar to SR)
|
||||||
|
if (displaySetInstanceUIDs) {
|
||||||
|
// clear the segmentation that we exported, similar to the storeMeasurement
|
||||||
|
// where we remove the measurements and prompt again the user if they would like
|
||||||
|
// to re-read the measurements in a SR read only viewport
|
||||||
|
segmentationService.remove(segmentationId);
|
||||||
|
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportId: viewportGridService.getActiveViewportId(),
|
||||||
|
displaySetInstanceUIDs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSegmentationDownloadRTSS = segmentationId => {
|
||||||
|
commandsManager.runCommand('downloadRTSS', {
|
||||||
|
segmentationId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSegmentationDownload = segmentationId => {
|
||||||
|
commandsManager.runCommand('downloadSegmentation', {
|
||||||
|
segmentationId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
||||||
|
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||||
<div className="mx-4 my-4 mb-4 flex space-x-4">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setLabelmapLoading(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
|
||||||
setLabelmapLoading(false);
|
|
||||||
setSelectedSegmentationId(segmentationId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{labelmapLoading ? 'loading ...' : 'New Label'}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleROIThresholding}>Run</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bg-secondary-dark border-secondary-light mb-2 flex h-8 cursor-pointer select-none items-center justify-around border-t outline-none first:border-0"
|
|
||||||
onClick={() => {
|
|
||||||
setShowConfig(!showConfig);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="px-4 text-base text-white">{t('ROI Threshold Configuration')}</div>
|
|
||||||
</div>
|
|
||||||
{showConfig && (
|
|
||||||
<ROIThresholdConfiguration
|
|
||||||
config={config}
|
|
||||||
dispatch={dispatch}
|
|
||||||
runCommand={runCommand}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* show segmentation table */}
|
{/* show segmentation table */}
|
||||||
<div className="mt-4">
|
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
|
||||||
{segmentations?.length ? (
|
<SegmentationGroupTableExpanded
|
||||||
<SegmentationTable
|
disableEditing={false}
|
||||||
title={t('Segmentations')}
|
showAddSegmentation={true}
|
||||||
|
showAddSegment={false}
|
||||||
|
showDeleteSegment={true}
|
||||||
segmentations={segmentations}
|
segmentations={segmentations}
|
||||||
activeSegmentationId={selectedSegmentationId}
|
onSegmentationAdd={onSegmentationAdd}
|
||||||
onClick={id => {
|
onSegmentationClick={onSegmentationClick}
|
||||||
runCommand('setSegmentationActiveForToolGroups', {
|
onToggleSegmentationVisibility={id => {
|
||||||
segmentationId: id,
|
|
||||||
});
|
|
||||||
setSelectedSegmentationId(id);
|
|
||||||
}}
|
|
||||||
onToggleVisibility={id => {
|
|
||||||
segmentationService.toggleSegmentationVisibility(id);
|
segmentationService.toggleSegmentationVisibility(id);
|
||||||
}}
|
}}
|
||||||
onToggleVisibilityAll={ids => {
|
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||||
ids.map(id => {
|
onSegmentationDelete={id => {
|
||||||
segmentationService.toggleSegmentationVisibility(id);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onDelete={id => {
|
|
||||||
segmentationService.remove(id);
|
segmentationService.remove(id);
|
||||||
}}
|
}}
|
||||||
onEdit={id => {
|
onSegmentationEdit={id => {
|
||||||
segmentationEditHandler({
|
segmentationEditHandler({
|
||||||
id,
|
id,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
segmentationConfig={{ initialConfig: segmentationService.getConfiguration() }}
|
||||||
|
onSegmentAdd={onSegmentAdd}
|
||||||
|
onSegmentClick={onSegmentClick}
|
||||||
|
onSegmentDelete={onSegmentDelete}
|
||||||
|
onSegmentEdit={onSegmentEdit}
|
||||||
|
onToggleSegmentLock={onToggleSegmentLock}
|
||||||
|
onSegmentColorClick={onSegmentColorClick}
|
||||||
|
storeSegmentation={storeSegmentation}
|
||||||
|
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||||
|
onSegmentationDownload={onSegmentationDownload}
|
||||||
|
setRenderOutline={value =>
|
||||||
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||||
|
}
|
||||||
|
setOutlineOpacityActive={value =>
|
||||||
|
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||||
|
}
|
||||||
|
setRenderFill={value =>
|
||||||
|
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||||
|
}
|
||||||
|
setRenderInactiveSegmentations={value =>
|
||||||
|
_setSegmentationConfiguration(
|
||||||
|
selectedSegmentationId,
|
||||||
|
'renderInactiveSegmentations',
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setOutlineWidthActive={value =>
|
||||||
|
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||||
|
}
|
||||||
|
setFillAlpha={value =>
|
||||||
|
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||||
|
}
|
||||||
|
setFillAlphaInactive={value =>
|
||||||
|
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
{tmtvValue !== null ? (
|
{tmtvValue !== null ? (
|
||||||
<div className="bg-secondary-dark mt-4 flex items-baseline justify-between px-2 py-1">
|
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
|
||||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||||
{'TMTV:'}
|
{'TMTV:'}
|
||||||
</span>
|
</span>
|
||||||
@ -251,7 +281,7 @@ export default function PanelRoiThresholdSegmentation({ servicesManager, command
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="mt-auto mb-4 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// navigate to a url in a new tab
|
// navigate to a url in a new tab
|
||||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||||
|
|||||||
@ -14,7 +14,7 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
|
|||||||
const { t } = useTranslation('ROIThresholdConfiguration');
|
const { t } = useTranslation('ROIThresholdConfiguration');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-primary-dark flex flex-col space-y-4 px-4 py-2">
|
<div className="bg-primary-dark flex flex-col space-y-4">
|
||||||
<div className="flex items-end space-x-2">
|
<div className="flex items-end space-x-2">
|
||||||
<div className="flex w-1/2 flex-col">
|
<div className="flex w-1/2 flex-col">
|
||||||
<Select
|
<Select
|
||||||
@ -62,8 +62,8 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
|
|||||||
{config.strategy === ROI_STAT && (
|
{config.strategy === ROI_STAT && (
|
||||||
<Input
|
<Input
|
||||||
label={t('Percentage of Max SUV')}
|
label={t('Percentage of Max SUV')}
|
||||||
labelClassName="text-white"
|
labelClassName="text-[13px] font-inter text-white"
|
||||||
className="border-primary-main mt-2 bg-black"
|
className="border-primary-main bg-black"
|
||||||
type="text"
|
type="text"
|
||||||
containerClassName="mr-2"
|
containerClassName="mr-2"
|
||||||
value={config.weight}
|
value={config.weight}
|
||||||
@ -83,11 +83,11 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr className="mt-2">
|
<tr className="mt-2">
|
||||||
<td
|
<td
|
||||||
className="pr-4 pt-2"
|
className="pr-4"
|
||||||
colSpan="3"
|
colSpan="3"
|
||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
className="text-white"
|
className="font-inter text-[13px] text-white"
|
||||||
text="Lower & Upper Ranges"
|
text="Lower & Upper Ranges"
|
||||||
></Label>
|
></Label>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
||||||
|
|
||||||
|
function callInputDialog(uiDialogService, label, callback) {
|
||||||
|
const dialogId = 'enter-segment-label';
|
||||||
|
|
||||||
|
const onSubmitHandler = ({ action, value }) => {
|
||||||
|
switch (action.id) {
|
||||||
|
case 'save':
|
||||||
|
callback(value.label, action.id);
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
callback('', action.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uiDialogService.dismiss({ id: dialogId });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uiDialogService) {
|
||||||
|
uiDialogService.create({
|
||||||
|
id: dialogId,
|
||||||
|
centralize: true,
|
||||||
|
isDraggable: false,
|
||||||
|
showOverlay: true,
|
||||||
|
content: Dialog,
|
||||||
|
contentProps: {
|
||||||
|
title: 'Segment',
|
||||||
|
value: { label },
|
||||||
|
noCloseButton: true,
|
||||||
|
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||||
|
actions: [
|
||||||
|
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||||
|
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
|
||||||
|
],
|
||||||
|
onSubmit: onSubmitHandler,
|
||||||
|
body: ({ value, setValue }) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
label="Enter the segment label"
|
||||||
|
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||||
|
autoFocus
|
||||||
|
className="border-primary-main bg-black"
|
||||||
|
type="text"
|
||||||
|
value={value.label}
|
||||||
|
onChange={event => {
|
||||||
|
event.persist();
|
||||||
|
setValue(value => ({ ...value, label: event.target.value }));
|
||||||
|
}}
|
||||||
|
onKeyPress={event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
onSubmitHandler({ value, action: { id: 'save' } });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default callInputDialog;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.chrome-picker {
|
||||||
|
background: #090c29 !important;
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dialog } from '@ohif/ui';
|
||||||
|
import { ChromePicker } from 'react-color';
|
||||||
|
|
||||||
|
import './colorPickerDialog.css';
|
||||||
|
|
||||||
|
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
|
||||||
|
const dialogId = 'pick-color';
|
||||||
|
|
||||||
|
const onSubmitHandler = ({ action, value }) => {
|
||||||
|
switch (action.id) {
|
||||||
|
case 'save':
|
||||||
|
callback(value.rgbaColor, action.id);
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
callback('', action.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uiDialogService.dismiss({ id: dialogId });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uiDialogService) {
|
||||||
|
uiDialogService.create({
|
||||||
|
id: dialogId,
|
||||||
|
centralize: true,
|
||||||
|
isDraggable: false,
|
||||||
|
showOverlay: true,
|
||||||
|
content: Dialog,
|
||||||
|
contentProps: {
|
||||||
|
title: 'Segment Color',
|
||||||
|
value: { rgbaColor },
|
||||||
|
noCloseButton: true,
|
||||||
|
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||||
|
actions: [
|
||||||
|
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||||
|
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||||
|
],
|
||||||
|
onSubmit: onSubmitHandler,
|
||||||
|
body: ({ value, setValue }) => {
|
||||||
|
const handleChange = color => {
|
||||||
|
setValue({ rgbaColor: color.rgb });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChromePicker
|
||||||
|
color={value.rgbaColor}
|
||||||
|
onChange={handleChange}
|
||||||
|
presetColors={[]}
|
||||||
|
width={300}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default callColorPickerDialog;
|
||||||
214
extensions/tmtv/src/Panels/RectangleROIOptions.tsx
Normal file
214
extensions/tmtv/src/Panels/RectangleROIOptions.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import React, { useState, useCallback, useReducer, useEffect } from 'react';
|
||||||
|
import { Button } from '@ohif/ui';
|
||||||
|
import ROIThresholdConfiguration, {
|
||||||
|
ROI_STAT,
|
||||||
|
} from './PanelROIThresholdSegmentation/ROIThresholdConfiguration';
|
||||||
|
import * as cs3dTools from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
||||||
|
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
||||||
|
const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
|
||||||
|
const UPPER_PT_THRESHOLD_DEFAULT = 100;
|
||||||
|
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
|
||||||
|
const DEFAULT_STRATEGY = ROI_STAT;
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
const { payload } = action;
|
||||||
|
const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'setStrategy':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
strategy,
|
||||||
|
};
|
||||||
|
case 'setThreshold':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
ctLower: ctLower ? ctLower : state.ctLower,
|
||||||
|
ctUpper: ctUpper ? ctUpper : state.ctUpper,
|
||||||
|
ptLower: ptLower ? ptLower : state.ptLower,
|
||||||
|
ptUpper: ptUpper ? ptUpper : state.ptUpper,
|
||||||
|
};
|
||||||
|
case 'setWeight':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
weight,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function RectangleROIOptions({ servicesManager, commandsManager }) {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||||
|
|
||||||
|
const runCommand = useCallback(
|
||||||
|
(commandName, commandOptions = {}) => {
|
||||||
|
return commandsManager.runCommand(commandName, commandOptions);
|
||||||
|
},
|
||||||
|
[commandsManager]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [config, dispatch] = useReducer(reducer, {
|
||||||
|
strategy: DEFAULT_STRATEGY,
|
||||||
|
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
|
||||||
|
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
|
||||||
|
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
|
||||||
|
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
|
||||||
|
weight: WEIGHT_DEFAULT,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleROIThresholding = useCallback(() => {
|
||||||
|
const segmentationId = selectedSegmentationId;
|
||||||
|
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
const activeSegmentIndex =
|
||||||
|
cs3dTools.segmentation.segmentIndex.getActiveSegmentIndex(segmentationId);
|
||||||
|
|
||||||
|
// run the threshold based on the active segment index
|
||||||
|
// Todo: later find a way to associate each rectangle with a segment (e.g., maybe with color?)
|
||||||
|
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
||||||
|
segmentationId,
|
||||||
|
config,
|
||||||
|
segmentIndex: activeSegmentIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
// re-calculating the cached stats for the active segmentation
|
||||||
|
const updatedPerSegmentCachedStats = {};
|
||||||
|
segmentation.segments = segmentation.segments.map(segment => {
|
||||||
|
if (!segment || !segment.segmentIndex) {
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentIndex = segment.segmentIndex;
|
||||||
|
|
||||||
|
const lesionStats = runCommand('getLesionStats', { labelmap, segmentIndex });
|
||||||
|
const suvPeak = runCommand('calculateSuvPeak', { labelmap, segmentIndex });
|
||||||
|
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||||
|
|
||||||
|
// update segDetails with the suv peak for the active segmentation
|
||||||
|
const cachedStats = {
|
||||||
|
lesionStats,
|
||||||
|
suvPeak,
|
||||||
|
lesionGlyoclysisStats,
|
||||||
|
};
|
||||||
|
|
||||||
|
segment.cachedStats = cachedStats;
|
||||||
|
segment.displayText = [
|
||||||
|
`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`,
|
||||||
|
`Volume: ${lesionStats.volume.toFixed(2)} mm3`,
|
||||||
|
];
|
||||||
|
updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
|
||||||
|
|
||||||
|
return segment;
|
||||||
|
});
|
||||||
|
|
||||||
|
const notYetUpdatedAtSource = true;
|
||||||
|
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
const tmtv = runCommand('calculateTMTV', { segmentations });
|
||||||
|
|
||||||
|
segmentation.cachedStats = Object.assign(
|
||||||
|
segmentation.cachedStats,
|
||||||
|
updatedPerSegmentCachedStats,
|
||||||
|
{
|
||||||
|
tmtv: {
|
||||||
|
value: tmtv.toFixed(3),
|
||||||
|
config: { ...config },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
segmentationService.addOrUpdateSegmentation(
|
||||||
|
{
|
||||||
|
...segmentation,
|
||||||
|
},
|
||||||
|
false, // don't suppress events
|
||||||
|
notYetUpdatedAtSource
|
||||||
|
);
|
||||||
|
}, [selectedSegmentationId, config]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
if (!segmentations.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = segmentations.find(seg => seg.isActive);
|
||||||
|
setSelectedSegmentationId(isActive.id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI based on segmentation changes (added, removed, updated)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
// ~~ Subscription
|
||||||
|
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||||
|
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||||
|
const subscriptions = [];
|
||||||
|
|
||||||
|
[added, updated].forEach(evt => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
if (!segmentations.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = segmentations.find(seg => seg.isActive);
|
||||||
|
setSelectedSegmentationId(isActive.id);
|
||||||
|
});
|
||||||
|
subscriptions.push(unsubscribe);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(unsub => {
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||||
|
() => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
if (segmentations.length > 0) {
|
||||||
|
setSelectedSegmentationId(segmentations[0].id);
|
||||||
|
handleROIThresholding();
|
||||||
|
} else {
|
||||||
|
setSelectedSegmentationId(null);
|
||||||
|
handleROIThresholding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="invisible-scrollbar mb-2 flex flex-col overflow-y-auto overflow-x-hidden">
|
||||||
|
<ROIThresholdConfiguration
|
||||||
|
config={config}
|
||||||
|
dispatch={dispatch}
|
||||||
|
runCommand={runCommand}
|
||||||
|
/>
|
||||||
|
{selectedSegmentationId !== null && (
|
||||||
|
<Button
|
||||||
|
className="mt-2 !h-[26px] !w-[75px]"
|
||||||
|
onClick={handleROIThresholding}
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RectangleROIOptions;
|
||||||
@ -112,6 +112,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
// Create a segmentation of the same resolution as the source data
|
// Create a segmentation of the same resolution as the source data
|
||||||
// using volumeLoader.createAndCacheDerivedVolume.
|
// using volumeLoader.createAndCacheDerivedVolume.
|
||||||
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||||
|
|
||||||
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
||||||
viewportMatchDetails,
|
viewportMatchDetails,
|
||||||
});
|
});
|
||||||
@ -121,8 +122,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentSegmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||||
ptDisplaySet.displaySetInstanceUID
|
ptDisplaySet.displaySetInstanceUID,
|
||||||
|
{ label: `Segmentation ${currentSegmentations.length + 1}` }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add Segmentation to all toolGroupIds in the viewer
|
// Add Segmentation to all toolGroupIds in the viewer
|
||||||
@ -141,6 +145,12 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
segmentationService.addSegment(segmentationId, {
|
||||||
|
segmentIndex: 1,
|
||||||
|
properties: {
|
||||||
|
label: 'Segment 1',
|
||||||
|
},
|
||||||
|
});
|
||||||
return segmentationId;
|
return segmentationId;
|
||||||
},
|
},
|
||||||
setSegmentationActiveForToolGroups: ({ segmentationId }) => {
|
setSegmentationActiveForToolGroups: ({ segmentationId }) => {
|
||||||
@ -150,7 +160,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
thresholdSegmentationByRectangleROITool: ({ segmentationId, config }) => {
|
thresholdSegmentationByRectangleROITool: ({ segmentationId, config, segmentIndex }) => {
|
||||||
const segmentation = csTools.segmentation.state.getSegmentation(segmentationId);
|
const segmentation = csTools.segmentation.state.getSegmentation(segmentationId);
|
||||||
|
|
||||||
const { representationData } = segmentation;
|
const { representationData } = segmentation;
|
||||||
@ -201,12 +211,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
{ volume: referencedVolume, lower: ptLower, upper: ptUpper },
|
{ volume: referencedVolume, lower: ptLower, upper: ptUpper },
|
||||||
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
|
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
|
||||||
],
|
],
|
||||||
{ overwrite: true }
|
{ overwrite: true, segmentIndex }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
calculateSuvPeak: ({ labelmap }) => {
|
calculateSuvPeak: ({ labelmap, segmentIndex }) => {
|
||||||
const { referencedVolumeId } = labelmap;
|
const { referencedVolumeId } = labelmap;
|
||||||
|
|
||||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||||
|
|
||||||
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
||||||
@ -217,7 +226,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
csTools.annotation.state.getAnnotation(annotationUID)
|
csTools.annotation.state.getAnnotation(annotationUID)
|
||||||
);
|
);
|
||||||
|
|
||||||
const suvPeak = calculateSuvPeak(labelmap, referencedVolume, annotations);
|
const suvPeak = calculateSuvPeak(labelmap, referencedVolume, annotations, segmentIndex);
|
||||||
return {
|
return {
|
||||||
suvPeak: suvPeak.mean,
|
suvPeak: suvPeak.mean,
|
||||||
suvMax: suvPeak.max,
|
suvMax: suvPeak.max,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels';
|
import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels';
|
||||||
|
import { Toolbox } from '@ohif/ui';
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - No loading UI exists yet
|
// - No loading UI exists yet
|
||||||
@ -12,18 +13,26 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
<PanelPetSUV
|
<PanelPetSUV
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappedROIThresholdSeg = () => {
|
const wrappedROIThresholdSeg = () => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Toolbox
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
buttonSectionId="tmtvToolbox"
|
||||||
|
title="Threshold Tools"
|
||||||
|
/>
|
||||||
<PanelROIThresholdSegmentation
|
<PanelROIThresholdSegmentation
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,15 +40,15 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
{
|
{
|
||||||
name: 'petSUV',
|
name: 'petSUV',
|
||||||
iconName: 'tab-patient-info',
|
iconName: 'tab-patient-info',
|
||||||
iconLabel: 'PET SUV',
|
iconLabel: 'Patient Info',
|
||||||
label: 'PET SUV',
|
label: 'Patient Info',
|
||||||
component: wrappedPanelPetSuv,
|
component: wrappedPanelPetSuv,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ROIThresholdSeg',
|
name: 'ROIThresholdSeg',
|
||||||
iconName: 'tab-roi-threshold',
|
iconName: 'tab-segmentation',
|
||||||
iconLabel: 'ROI Threshold',
|
iconLabel: 'Segmentation',
|
||||||
label: 'ROI Threshold',
|
label: 'Segmentation',
|
||||||
component: wrappedROIThresholdSeg,
|
component: wrappedROIThresholdSeg,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
10
extensions/tmtv/src/getToolbarModule.tsx
Normal file
10
extensions/tmtv/src/getToolbarModule.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import RectangleROIOptions from './Panels/RectangleROIOptions';
|
||||||
|
|
||||||
|
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'tmtv.RectangleROIThresholdOptions',
|
||||||
|
defaultComponent: () => RectangleROIOptions({ commandsManager, servicesManager }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import getHangingProtocolModule from './getHangingProtocolModule';
|
|||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import init from './init';
|
import init from './init';
|
||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
|
import getToolbarModule from './getToolbarModule';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -15,6 +16,7 @@ const tmtvExtension = {
|
|||||||
preRegistration({ servicesManager, commandsManager, extensionManager, configuration = {} }) {
|
preRegistration({ servicesManager, commandsManager, extensionManager, configuration = {} }) {
|
||||||
init({ servicesManager, commandsManager, extensionManager, configuration });
|
init({ servicesManager, commandsManager, extensionManager, configuration });
|
||||||
},
|
},
|
||||||
|
getToolbarModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||||
|
|||||||
@ -43,8 +43,6 @@ const RectangleROIStartEndThreshold = {
|
|||||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cachedStats } = data;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid: annotationUID,
|
uid: annotationUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
@ -56,10 +54,8 @@ const RectangleROIStartEndThreshold = {
|
|||||||
toolName: metadata.toolName,
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: metadata.label,
|
label: metadata.label,
|
||||||
// displayText: displayText,
|
|
||||||
data: data.cachedStats,
|
data: data.cachedStats,
|
||||||
type: 'RectangleROIStartEndThreshold',
|
type: 'RectangleROIStartEndThreshold',
|
||||||
// getReport,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -119,7 +119,10 @@ const toolbarButtons: Button[] = [
|
|||||||
icon: 'tool-3d-rotate',
|
icon: 'tool-3d-rotate',
|
||||||
label: '3D Rotate',
|
label: '3D Rotate',
|
||||||
commands: setToolActiveToolbar,
|
commands: setToolActiveToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstoneTool',
|
||||||
|
disabledText: 'Select a 3D viewport to enable this tool',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -154,7 +157,10 @@ const toolbarButtons: Button[] = [
|
|||||||
toolGroupIds: ['mpr'],
|
toolGroupIds: ['mpr'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstoneTool',
|
||||||
|
disabledText: 'Select an MPR viewport to enable this tool',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const toolbarButtons: Button[] = [
|
|||||||
evaluate: {
|
evaluate: {
|
||||||
name: 'evaluate.cornerstone.segmentation',
|
name: 'evaluate.cornerstone.segmentation',
|
||||||
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||||
|
disabledText: 'Create new segmentation to enable this tool.',
|
||||||
},
|
},
|
||||||
commands: _createSetToolActiveCommands('CircularBrush'),
|
commands: _createSetToolActiveCommands('CircularBrush'),
|
||||||
options: [
|
options: [
|
||||||
@ -95,7 +96,7 @@ const toolbarButtons: Button[] = [
|
|||||||
{
|
{
|
||||||
id: 'Threshold',
|
id: 'Threshold',
|
||||||
icon: 'icon-tool-threshold',
|
icon: 'icon-tool-threshold',
|
||||||
label: 'Eraser',
|
label: 'Threshold Tool',
|
||||||
evaluate: {
|
evaluate: {
|
||||||
name: 'evaluate.cornerstone.segmentation',
|
name: 'evaluate.cornerstone.segmentation',
|
||||||
options: { toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'] },
|
options: { toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'] },
|
||||||
|
|||||||
@ -75,7 +75,10 @@ const toolbarButtons: Button[] = [
|
|||||||
icon: 'tool-3d-rotate',
|
icon: 'tool-3d-rotate',
|
||||||
label: '3D Rotate',
|
label: '3D Rotate',
|
||||||
commands: setToolActiveToolbar,
|
commands: setToolActiveToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstoneTool',
|
||||||
|
disabledText: 'Select a 3D viewport to enable this tool',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -110,7 +113,10 @@ const toolbarButtons: Button[] = [
|
|||||||
toolGroupIds: ['mpr'],
|
toolGroupIds: ['mpr'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstoneTool',
|
||||||
|
disabledText: 'Select an MPR viewport to enable this tool',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -89,8 +89,8 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
'Crosshairs',
|
'Crosshairs',
|
||||||
'Pan',
|
'Pan',
|
||||||
'SyncToggle',
|
'SyncToggle',
|
||||||
'RectangleROIStartEndThreshold',
|
|
||||||
]);
|
]);
|
||||||
|
toolbarService.createButtonSection('tmtvToolbox', ['RectangleROIStartEndThreshold']);
|
||||||
|
|
||||||
// For the hanging protocol we need to decide on the window level
|
// For the hanging protocol we need to decide on the window level
|
||||||
// based on whether the SUV is corrected or not, hence we can't hard
|
// based on whether the SUV is corrected or not, hence we can't hard
|
||||||
|
|||||||
@ -1,42 +1,8 @@
|
|||||||
import { defaults, ToolbarService } from '@ohif/core';
|
import { defaults, ToolbarService } from '@ohif/core';
|
||||||
import { WindowLevelMenuItem } from '@ohif/ui';
|
|
||||||
import { toolGroupIds } from './initToolGroups';
|
import { toolGroupIds } from './initToolGroups';
|
||||||
|
|
||||||
const { windowLevelPresets } = defaults;
|
const { windowLevelPresets } = defaults;
|
||||||
|
|
||||||
function _createColormap(label, colormap) {
|
|
||||||
return {
|
|
||||||
id: label,
|
|
||||||
label,
|
|
||||||
type: 'action',
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
commandName: 'setFusionPTColormap',
|
|
||||||
commandOptions: {
|
|
||||||
toolGroupId: toolGroupIds.Fusion,
|
|
||||||
colormap,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function _createWwwcPreset(preset, title, subtitle) {
|
|
||||||
return {
|
|
||||||
id: preset.toString(),
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
commandName: 'setWindowLevel',
|
|
||||||
commandOptions: {
|
|
||||||
...windowLevelPresets[preset],
|
|
||||||
},
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const setToolActiveToolbar = {
|
const setToolActiveToolbar = {
|
||||||
commandName: 'setToolActiveToolbar',
|
commandName: 'setToolActiveToolbar',
|
||||||
commandOptions: {
|
commandOptions: {
|
||||||
@ -141,7 +107,11 @@ const toolbarButtons = [
|
|||||||
icon: 'tool-create-threshold',
|
icon: 'tool-create-threshold',
|
||||||
label: 'Rectangle ROI Threshold',
|
label: 'Rectangle ROI Threshold',
|
||||||
commands: setToolActiveToolbar,
|
commands: setToolActiveToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstoneTool',
|
||||||
|
disabledText: 'Select the PT Axial to enable this tool',
|
||||||
|
},
|
||||||
|
options: 'tmtv.RectangleROIThresholdOptions',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -233,7 +233,8 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||||
|
<div id="react-portal"></div>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -204,6 +204,7 @@ export default class ToolbarService extends PubSubService {
|
|||||||
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
||||||
const updatedProps = {
|
const updatedProps = {
|
||||||
...props,
|
...props,
|
||||||
|
...evaluated,
|
||||||
disabled: evaluated?.disabled || false,
|
disabled: evaluated?.disabled || false,
|
||||||
className: evaluated?.className || '',
|
className: evaluated?.className || '',
|
||||||
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
|
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
|
||||||
@ -386,7 +387,9 @@ export default class ToolbarService extends PubSubService {
|
|||||||
const { id, uiType, component } = btn;
|
const { id, uiType, component } = btn;
|
||||||
const { groupId } = btn.props;
|
const { groupId } = btn.props;
|
||||||
|
|
||||||
const buttonType = this._getButtonUITypes()[uiType];
|
const buttonTypes = this._getButtonUITypes();
|
||||||
|
|
||||||
|
const buttonType = buttonTypes[uiType];
|
||||||
|
|
||||||
if (!buttonType) {
|
if (!buttonType) {
|
||||||
return;
|
return;
|
||||||
@ -414,7 +417,16 @@ export default class ToolbarService extends PubSubService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleEvaluate = props => {
|
handleEvaluate = props => {
|
||||||
const { evaluate } = props;
|
const { evaluate, options } = props;
|
||||||
|
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
// get the custom option component from the extension manager and set it as the optionComponent
|
||||||
|
const buttonTypes = this._getButtonUITypes();
|
||||||
|
const optionComponent = buttonTypes[options]?.defaultComponent;
|
||||||
|
props.options = {
|
||||||
|
optionComponent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof evaluate === 'function') {
|
if (typeof evaluate === 'function') {
|
||||||
return;
|
return;
|
||||||
@ -462,9 +474,8 @@ export default class ToolbarService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof evaluate === 'object') {
|
if (typeof evaluate === 'object') {
|
||||||
const { name, options } = evaluate;
|
const { name, ...options } = evaluate;
|
||||||
const evaluateFunction = this._evaluateFunction[name];
|
const evaluateFunction = this._evaluateFunction[name];
|
||||||
|
|
||||||
if (evaluateFunction) {
|
if (evaluateFunction) {
|
||||||
props.evaluate = args => evaluateFunction({ ...args, ...options });
|
props.evaluate = args => evaluateFunction({ ...args, ...options });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -97,6 +97,7 @@ Let's look at one of the evaluators (for `evaluate.cornerstoneTool`)
|
|||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
className: '!text-common-bright ohif-disabled',
|
className: '!text-common-bright ohif-disabled',
|
||||||
|
disabledText: 'Tool not available',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +372,104 @@ state will get synchronized with the toolbar service automatically.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Toolbox With Options
|
||||||
|
|
||||||
|
Your toolbox toolbar buttons can have options, this is really useful
|
||||||
|
for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D).
|
||||||
|
|
||||||
|
currently we support three types of options
|
||||||
|
|
||||||
|
### Radio option
|
||||||
|
|
||||||
|
We use this in segmentation shapes to let the user choose between
|
||||||
|
three different modes
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: 'Shapes',
|
||||||
|
uiType: 'ohif.radioGroup',
|
||||||
|
props: {
|
||||||
|
label: 'Shapes',
|
||||||
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstone.segmentation',
|
||||||
|
options: { toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'] },
|
||||||
|
},
|
||||||
|
icon: 'icon-tool-shape',
|
||||||
|
commands: _createSetToolActiveCommands('CircleScissor'),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Shape',
|
||||||
|
type: 'radio',
|
||||||
|
value: 'CircleScissor',
|
||||||
|
id: 'shape-mode',
|
||||||
|
values: [
|
||||||
|
{ value: 'CircleScissor', label: 'Circle' },
|
||||||
|
{ value: 'SphereScissor', label: 'Sphere' },
|
||||||
|
{ value: 'RectangleScissor', label: 'Rectangle' },
|
||||||
|
],
|
||||||
|
commands: 'setToolActiveToolbar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Range option
|
||||||
|
|
||||||
|
We use this for brush radius change
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: 'Brush',
|
||||||
|
icon: 'icon-tool-brush',
|
||||||
|
label: 'Brush',
|
||||||
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstone.segmentation',
|
||||||
|
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||||
|
disabledText: 'Create new segmentation to enable this tool.',
|
||||||
|
},
|
||||||
|
commands: _createSetToolActiveCommands('CircularBrush'),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Radius (mm)',
|
||||||
|
id: 'brush-radius',
|
||||||
|
type: 'range',
|
||||||
|
min: 0.5,
|
||||||
|
max: 99.5,
|
||||||
|
step: 0.5,
|
||||||
|
value: 25,
|
||||||
|
commands: {
|
||||||
|
commandName: 'setBrushSize',
|
||||||
|
commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom option
|
||||||
|
|
||||||
|
We use this pattern inside `tmtv` mode for `RectangleROIThreshold`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: 'RectangleROIStartEndThreshold',
|
||||||
|
uiType: 'ohif.radioGroup',
|
||||||
|
props: {
|
||||||
|
icon: 'tool-create-threshold',
|
||||||
|
label: 'Rectangle ROI Threshold',
|
||||||
|
commands: setToolActiveToolbar,
|
||||||
|
evaluate: {
|
||||||
|
name: 'evaluate.cornerstoneTool',
|
||||||
|
disabledText: 'Select the PT Axial to enable this tool',
|
||||||
|
},
|
||||||
|
options: 'tmtv.RectangleROIThresholdOptions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it is your job to provide the `tmvt.RectangleROIThresholdOptions` in the getToolbarModule of your extension
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Change Toolbar with hanging protocols
|
## Change Toolbar with hanging protocols
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
/* CUSTOM OHIF SCROLLBAR */
|
/* CUSTOM OHIF SCROLLBAR */
|
||||||
.ohif-scrollbar {
|
.ohif-scrollbar {
|
||||||
scrollbar-color: #041c4a transparent;
|
scrollbar-color: #173239 transparent;
|
||||||
scrollbar-gutter: stable;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.study-min-height {
|
.study-min-height {
|
||||||
|
|||||||
@ -13,6 +13,10 @@ function ToolSettings({ options }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
return options();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 py-2 text-white">
|
<div className="space-y-2 py-2 text-white">
|
||||||
{options?.map(option => {
|
{options?.map(option => {
|
||||||
|
|||||||
@ -27,15 +27,14 @@ const ButtonGroup = ({
|
|||||||
vertical: 'flex-col',
|
vertical: 'flex-col',
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapperClasses = classnames('inline-flex', orientationClasses[orientation], className);
|
const wrapperClasses = classnames(
|
||||||
|
'items-stretch inline-flex',
|
||||||
|
orientationClasses[orientation],
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classnames(wrapperClasses, 'text-[13px]')}>
|
||||||
className={classnames(
|
|
||||||
wrapperClasses,
|
|
||||||
'border-secondary-light rounded-[5px] border bg-black text-[13px]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{Children.map(children, (child, index) => {
|
{Children.map(children, (child, index) => {
|
||||||
if (React.isValidElement(child)) {
|
if (React.isValidElement(child)) {
|
||||||
return cloneElement(child, {
|
return cloneElement(child, {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useCallback, useState, useRef } from 'react';
|
import React, { useEffect, useCallback, useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
import Typography from '../Typography';
|
import Typography from '../Typography';
|
||||||
@ -21,7 +22,9 @@ const Dropdown = ({
|
|||||||
maxCharactersPerLine,
|
maxCharactersPerLine,
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const element = useRef(null);
|
const elementRef = useRef(null);
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
const [coords, setCoords] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
// choose the max characters per line based on the longest title
|
// choose the max characters per line based on the longest title
|
||||||
const longestTitle = list.reduce((acc, item) => {
|
const longestTitle = list.reduce((acc, item) => {
|
||||||
@ -107,23 +110,54 @@ const Dropdown = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = e => {
|
const handleClick = e => {
|
||||||
if (element.current && !element.current.contains(e.target)) {
|
if (elementRef.current && !elementRef.current.contains(e.target)) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (elementRef.current && dropdownRef.current) {
|
||||||
|
const triggerRect = elementRef.current.getBoundingClientRect();
|
||||||
|
const dropdownRect = dropdownRef.current.getBoundingClientRect();
|
||||||
|
let x, y;
|
||||||
|
|
||||||
|
switch (alignment) {
|
||||||
|
case 'right':
|
||||||
|
x = triggerRect.right + window.scrollX - dropdownRect.width;
|
||||||
|
y = triggerRect.bottom + window.scrollY;
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
x = triggerRect.left + window.scrollX;
|
||||||
|
y = triggerRect.bottom + window.scrollY;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
x = triggerRect.left + window.scrollX;
|
||||||
|
y = triggerRect.bottom + window.scrollY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setCoords({ x, y });
|
||||||
|
}
|
||||||
|
}, [open, alignment, elementRef.current, dropdownRef.current]);
|
||||||
|
|
||||||
const renderList = () => {
|
const renderList = () => {
|
||||||
return (
|
const portalElement = document.getElementById('react-portal');
|
||||||
|
|
||||||
|
const listElement = (
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'top-100 border-secondary-main absolute z-10 mt-2 transform rounded border bg-black shadow transition duration-300',
|
'top-100 border-secondary-main w-max-content absolute mt-2 transform rounded border bg-black shadow transition duration-300',
|
||||||
{
|
{
|
||||||
'right-0 origin-top-right': alignment === 'right',
|
'right-0 origin-top-right': alignment === 'right',
|
||||||
'left-0 origin-top-left': alignment === 'left',
|
'left-0 origin-top-left': alignment === 'left',
|
||||||
'scale-0': !open,
|
|
||||||
'scale-100': open,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
ref={dropdownRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: `${coords.y}px`,
|
||||||
|
left: open ? `${coords.x}px` : -999999,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
data-cy={`${id}-dropdown`}
|
data-cy={`${id}-dropdown`}
|
||||||
>
|
>
|
||||||
{list.map((item, idx) => (
|
{list.map((item, idx) => (
|
||||||
@ -137,6 +171,7 @@ const Dropdown = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
return ReactDOM.createPortal(listElement, portalElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -150,7 +185,7 @@ const Dropdown = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-cy="dropdown"
|
data-cy="dropdown"
|
||||||
ref={element}
|
ref={elementRef}
|
||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -33,6 +33,7 @@ const Input = ({
|
|||||||
onKeyDown,
|
onKeyDown,
|
||||||
readOnly,
|
readOnly,
|
||||||
disabled,
|
disabled,
|
||||||
|
labelChildren,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -40,6 +41,7 @@ const Input = ({
|
|||||||
<Label
|
<Label
|
||||||
className={labelClassName}
|
className={labelClassName}
|
||||||
text={label}
|
text={label}
|
||||||
|
children={labelChildren}
|
||||||
></Label>
|
></Label>
|
||||||
<input
|
<input
|
||||||
data-cy={`input-${id}`}
|
data-cy={`input-${id}`}
|
||||||
@ -83,6 +85,7 @@ Input.propTypes = {
|
|||||||
onKeyPress: PropTypes.func,
|
onKeyPress: PropTypes.func,
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
labelChildren: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|||||||
@ -37,7 +37,6 @@ const PanelSection = ({ title, children, actionIcons = [] }) => {
|
|||||||
</div>
|
</div>
|
||||||
{areChildrenVisible && (
|
{areChildrenVisible && (
|
||||||
<>
|
<>
|
||||||
<div className="h-[2px] bg-black"></div>
|
|
||||||
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
|
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,15 +2,14 @@ import React from 'react';
|
|||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function AddSegmentRow({ onClick }) {
|
function AddSegmentRow({ onClick, onToggleSegmentationVisibility = null, segmentation = null }) {
|
||||||
const { t } = useTranslation('SegmentationTable');
|
const { t } = useTranslation('SegmentationTable');
|
||||||
return (
|
return (
|
||||||
|
<div className="flex justify-between bg-black pl-[34px] hover:cursor-pointer">
|
||||||
<div
|
<div
|
||||||
className="flex hover:cursor-pointer"
|
className="group pt-[5px] pb-[5px]"
|
||||||
onClick={onClick}
|
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="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">
|
<div className="grid h-[28px] w-[28px] place-items-center">
|
||||||
<Icon name="icon-add" />
|
<Icon name="icon-add" />
|
||||||
@ -18,6 +17,26 @@ function AddSegmentRow({ onClick }) {
|
|||||||
<span className="text-[13px]">{t('Add segment')}</span>
|
<span className="text-[13px]">{t('Add segment')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{segmentation && (
|
||||||
|
<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(segmentation.id)}
|
||||||
|
>
|
||||||
|
{segmentation.isVisible ? (
|
||||||
|
<Icon
|
||||||
|
name="row-shown"
|
||||||
|
className="text-primary-active"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
name="row-hidden"
|
||||||
|
className="text-primary-active"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Select, Icon, Dropdown } from '../../components';
|
import { Select, Icon, Dropdown, Tooltip } from '../../components';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ function SegmentationDropDownRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group mx-0.5 mt-[8px] flex items-center">
|
<div className="group mx-0.5 mt-[8px] flex items-center pb-[10px]">
|
||||||
<div
|
<div
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -97,7 +97,7 @@ function SegmentationDropDownRow({
|
|||||||
],
|
],
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className="hover:bg-secondary-dark mx-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
|
<div className="hover:bg-secondary-dark grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
|
||||||
<Icon name="icon-more-menu"></Icon>
|
<Icon name="icon-more-menu"></Icon>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@ -122,8 +122,22 @@ function SegmentationDropDownRow({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
<Tooltip
|
||||||
|
position="bottom-right"
|
||||||
|
content={
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-[13px] text-white">Series:</div>
|
||||||
|
<div className="text-aqua-pale text-[13px]">{activeSegmentation.description}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="info-action"
|
||||||
|
className="text-primary-active"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<div
|
<div
|
||||||
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
|
className="hover:bg-secondary-dark mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
|
||||||
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
|
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
|
||||||
>
|
>
|
||||||
{activeSegmentation.isVisible ? (
|
{activeSegmentation.isVisible ? (
|
||||||
|
|||||||
@ -20,6 +20,7 @@ const SegmentItem = ({
|
|||||||
onColor,
|
onColor,
|
||||||
onToggleVisibility,
|
onToggleVisibility,
|
||||||
onToggleLocked,
|
onToggleLocked,
|
||||||
|
displayText,
|
||||||
}) => {
|
}) => {
|
||||||
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
|
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
|
||||||
|
|
||||||
@ -27,7 +28,9 @@ const SegmentItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('text-aqua-pale group/row flex min-h-[28px] bg-black')}
|
className={classnames('text-aqua-pale group/row bg-primary-dark flex min-h-[28px] flex-col', {
|
||||||
|
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
|
||||||
|
})}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick(segmentationId, segmentIndex);
|
onClick(segmentationId, segmentIndex);
|
||||||
@ -35,10 +38,11 @@ const SegmentItem = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-cy={'segment-item'}
|
data-cy={'segment-item'}
|
||||||
>
|
>
|
||||||
|
<div className="flex min-h-[28px]">
|
||||||
<div
|
<div
|
||||||
className={classnames('bg-primary-dark group/number grid w-[32px] place-items-center', {
|
className={classnames('group/number grid w-[28px] place-items-center', {
|
||||||
'!bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
|
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
|
||||||
'border-primary-dark border': !isActive,
|
'bg-primary-dark border-primary-dark border': !isActive,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => setIsNumberBoxHovering(true)}
|
onMouseEnter={() => setIsNumberBoxHovering(true)}
|
||||||
onMouseLeave={() => setIsNumberBoxHovering(false)}
|
onMouseLeave={() => setIsNumberBoxHovering(false)}
|
||||||
@ -62,12 +66,18 @@ const SegmentItem = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classnames('relative flex w-full', {
|
className=" h-[30px] bg-black"
|
||||||
'border-primary-light bg-primary-dark rounded-r-[4px] border border-l-0': isActive,
|
style={{ width: '3px' }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classnames('text-aqua-pale relative flex w-full', {
|
||||||
'border border-l-0 border-transparent': !isActive,
|
'border border-l-0 border-transparent': !isActive,
|
||||||
})}
|
})}
|
||||||
|
style={{
|
||||||
|
width: 'calc(100% - 40px)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="group-hover/row:bg-primary-dark flex h-full w-full flex-grow items-center">
|
<div className="bg-primary-dark flex h-full flex-grow items-center">
|
||||||
<div className="pl-2 pr-1.5">
|
<div className="pl-2 pr-1.5">
|
||||||
<div
|
<div
|
||||||
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
|
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
|
||||||
@ -144,6 +154,25 @@ const SegmentItem = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{Array.isArray(displayText) ? (
|
||||||
|
<div className="flex flex-col py-[5px] pl-[43px]">
|
||||||
|
{displayText.map(text => (
|
||||||
|
<div
|
||||||
|
key={text}
|
||||||
|
className="text-aqua-pale flex h-full items-center bg-black text-[11px]"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
displayText && (
|
||||||
|
<div className="text-aqua-pale flex h-full items-center bg-black px-2 py-[5px] pl-[45px] text-[11px]">
|
||||||
|
{displayText}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -205,6 +234,7 @@ SegmentItem.propTypes = {
|
|||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onToggleVisibility: PropTypes.func.isRequired,
|
onToggleVisibility: PropTypes.func.isRequired,
|
||||||
onToggleLocked: PropTypes.func,
|
onToggleLocked: PropTypes.func,
|
||||||
|
displayText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
SegmentItem.defaultProps = {
|
SegmentItem.defaultProps = {
|
||||||
|
|||||||
@ -100,7 +100,7 @@ const SegmentationGroupTable = ({
|
|||||||
)}
|
)}
|
||||||
<div className="bg-primary-dark">
|
<div className="bg-primary-dark">
|
||||||
{segmentations?.length === 0 ? (
|
{segmentations?.length === 0 ? (
|
||||||
<div className="select-none rounded-[4px]">
|
<div className="select-none bg-black pt-[5px] pb-[5px]">
|
||||||
{showAddSegmentation && !disableEditing && (
|
{showAddSegmentation && !disableEditing && (
|
||||||
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
|
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
|
||||||
)}
|
)}
|
||||||
@ -127,7 +127,7 @@ const SegmentationGroupTable = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{activeSegmentation && (
|
{activeSegmentation && (
|
||||||
<div className="ohif-scrollbar mt-1.5 flex min-h-0 flex-col overflow-y-hidden">
|
<div className="ohif-scrollbar flex h-fit max-h-[500px] min-h-0 flex-col overflow-auto bg-black">
|
||||||
{activeSegmentation?.segments?.map(segment => {
|
{activeSegmentation?.segments?.map(segment => {
|
||||||
if (!segment) {
|
if (!segment) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -0,0 +1,211 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PanelSection } from '../../components';
|
||||||
|
import SegmentationConfig from './SegmentationConfig';
|
||||||
|
import NoSegmentationRow from './NoSegmentationRow';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import SegmentationItem from './SegmentationItem';
|
||||||
|
|
||||||
|
const SegmentationGroupTableExpanded = ({
|
||||||
|
segmentations,
|
||||||
|
// segmentation initial config
|
||||||
|
segmentationConfig,
|
||||||
|
// UI show/hide
|
||||||
|
disableEditing,
|
||||||
|
showAddSegmentation,
|
||||||
|
showAddSegment,
|
||||||
|
showDeleteSegment,
|
||||||
|
// segmentation/segment handlers
|
||||||
|
onSegmentationAdd,
|
||||||
|
onSegmentationEdit,
|
||||||
|
onSegmentationClick,
|
||||||
|
onSegmentationDelete,
|
||||||
|
onSegmentationDownload,
|
||||||
|
onSegmentationDownloadRTSS,
|
||||||
|
storeSegmentation,
|
||||||
|
// segment handlers
|
||||||
|
onSegmentClick,
|
||||||
|
onSegmentAdd,
|
||||||
|
onSegmentDelete,
|
||||||
|
onSegmentEdit,
|
||||||
|
onToggleSegmentationVisibility,
|
||||||
|
onToggleSegmentVisibility,
|
||||||
|
onToggleSegmentLock,
|
||||||
|
onSegmentColorClick,
|
||||||
|
// segmentation config handlers
|
||||||
|
setFillAlpha,
|
||||||
|
setFillAlphaInactive,
|
||||||
|
setOutlineWidthActive,
|
||||||
|
setOutlineOpacityActive,
|
||||||
|
setRenderFill,
|
||||||
|
setRenderInactiveSegmentations,
|
||||||
|
setRenderOutline,
|
||||||
|
}) => {
|
||||||
|
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||||
|
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
const { t } = useTranslation('SegmentationTable');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
|
||||||
|
<PanelSection
|
||||||
|
title={t('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="bg-primary-dark">
|
||||||
|
<div className="select-none bg-black pt-[5px] pb-[5px]">
|
||||||
|
{showAddSegmentation && !disableEditing && (
|
||||||
|
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{segmentations?.length > 0 && (
|
||||||
|
<div className="ohif-scrollbar flex max-h-[500px] select-none flex-col gap-[5px] overflow-auto bg-black">
|
||||||
|
{segmentations?.map(segmentation => {
|
||||||
|
return (
|
||||||
|
<div key={segmentation.id}>
|
||||||
|
<SegmentationItem
|
||||||
|
key={segmentation.id}
|
||||||
|
segmentation={segmentation}
|
||||||
|
disableEditing={disableEditing}
|
||||||
|
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||||
|
onSegmentationEdit={onSegmentationEdit}
|
||||||
|
onSegmentationDelete={onSegmentationDelete}
|
||||||
|
onSegmentationDownload={onSegmentationDownload}
|
||||||
|
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||||
|
storeSegmentation={storeSegmentation}
|
||||||
|
showAddSegment={showAddSegment}
|
||||||
|
onSegmentAdd={onSegmentAdd}
|
||||||
|
onSegmentClick={onSegmentClick}
|
||||||
|
onSegmentDelete={onSegmentDelete}
|
||||||
|
onSegmentEdit={onSegmentEdit}
|
||||||
|
showDeleteSegment={showDeleteSegment}
|
||||||
|
onSegmentColorClick={onSegmentColorClick}
|
||||||
|
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||||
|
onToggleSegmentLock={onToggleSegmentLock}
|
||||||
|
activeSegmentationId={activeSegmentationId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PanelSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SegmentationGroupTableExpanded.propTypes = {
|
||||||
|
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,
|
||||||
|
onSegmentationDownloadRTSS: PropTypes.func,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
SegmentationGroupTableExpanded.defaultProps = {
|
||||||
|
segmentations: [],
|
||||||
|
disableEditing: false,
|
||||||
|
showAddSegmentation: true,
|
||||||
|
showAddSegment: true,
|
||||||
|
showDeleteSegment: true,
|
||||||
|
onSegmentationAdd: () => {},
|
||||||
|
onSegmentationEdit: () => {},
|
||||||
|
onSegmentationClick: () => {},
|
||||||
|
onSegmentationDelete: () => {},
|
||||||
|
onSegmentationDownload: () => {},
|
||||||
|
onSemgnetationDownloadRTSS: () => {},
|
||||||
|
storeSegmentation: () => {},
|
||||||
|
onSegmentClick: () => {},
|
||||||
|
onSegmentAdd: () => {},
|
||||||
|
onSegmentDelete: () => {},
|
||||||
|
onSegmentEdit: () => {},
|
||||||
|
onToggleSegmentationVisibility: () => {},
|
||||||
|
onToggleSegmentVisibility: () => {},
|
||||||
|
onToggleSegmentLock: () => {},
|
||||||
|
onSegmentColorClick: () => {},
|
||||||
|
setFillAlpha: () => {},
|
||||||
|
setFillAlphaInactive: () => {},
|
||||||
|
setOutlineWidthActive: () => {},
|
||||||
|
setOutlineOpacityActive: () => {},
|
||||||
|
setRenderFill: () => {},
|
||||||
|
setRenderInactiveSegmentations: () => {},
|
||||||
|
setRenderOutline: () => {},
|
||||||
|
};
|
||||||
|
export default SegmentationGroupTableExpanded;
|
||||||
@ -0,0 +1,211 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Icon, Dropdown } from '../../components';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import AddSegmentRow from './AddSegmentRow';
|
||||||
|
import SegmentationGroupSegment from './SegmentationGroupSegment';
|
||||||
|
import { Tooltip } from '../../components';
|
||||||
|
|
||||||
|
function SegmentationItem({
|
||||||
|
segmentation,
|
||||||
|
disableEditing,
|
||||||
|
onSegmentationEdit,
|
||||||
|
onSegmentationDownload,
|
||||||
|
onSegmentationDownloadRTSS,
|
||||||
|
storeSegmentation,
|
||||||
|
onSegmentationDelete,
|
||||||
|
showAddSegment,
|
||||||
|
onToggleSegmentationVisibility,
|
||||||
|
onSegmentAdd,
|
||||||
|
onSegmentClick,
|
||||||
|
onSegmentDelete,
|
||||||
|
onSegmentEdit,
|
||||||
|
showDeleteSegment,
|
||||||
|
onSegmentColorClick,
|
||||||
|
onToggleSegmentVisibility,
|
||||||
|
onToggleSegmentLock,
|
||||||
|
activeSegmentationId,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation('SegmentationTable');
|
||||||
|
|
||||||
|
const [areChildrenVisible, setChildrenVisible] = useState(true);
|
||||||
|
|
||||||
|
const handleHeaderClick = () => {
|
||||||
|
setChildrenVisible(!areChildrenVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-secondary-dark group relative flex items-center justify-start gap-1">
|
||||||
|
<div
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex"
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
id="segmentation-dropdown"
|
||||||
|
showDropdownIcon={false}
|
||||||
|
alignment="left"
|
||||||
|
itemsClassName="text-primary-active"
|
||||||
|
showBorders={false}
|
||||||
|
maxCharactersPerLine={30}
|
||||||
|
list={[
|
||||||
|
...(!disableEditing
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t('Rename'),
|
||||||
|
onClick: () => {
|
||||||
|
onSegmentationEdit(segmentation.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: t('Delete'),
|
||||||
|
onClick: () => {
|
||||||
|
onSegmentationDelete(segmentation.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(!disableEditing
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t('Export DICOM SEG'),
|
||||||
|
onClick: () => {
|
||||||
|
storeSegmentation(segmentation.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
title: t('Download DICOM SEG'),
|
||||||
|
onClick: () => {
|
||||||
|
onSegmentationDownload(segmentation.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Download DICOM RTSTRUCT'),
|
||||||
|
onClick: () => {
|
||||||
|
onSegmentationDownloadRTSS(segmentation.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="hover:bg-secondary-dark grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
|
||||||
|
<Icon name="icon-more-menu"></Icon>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
<div
|
||||||
|
className=" h-[28px] bg-black"
|
||||||
|
style={{ width: '3px' }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex h-full w-full cursor-pointer items-center justify-between pr-[8px]"
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
>
|
||||||
|
<div className="font-inter text-aqua-pale text-[13px]">{segmentation.label}</div>
|
||||||
|
<div className="flex h-[28px] items-center justify-center gap-2">
|
||||||
|
<Tooltip
|
||||||
|
position="bottom-right"
|
||||||
|
content={
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-[13px] text-white">Series:</div>
|
||||||
|
<div className="text-aqua-pale text-[13px]">{segmentation.description}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="info-action"
|
||||||
|
className="text-primary-active"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<div className={areChildrenVisible ? '' : 'mr-[4px]'}>
|
||||||
|
<Icon name={areChildrenVisible ? 'chevron-down-new' : 'chevron-left-new'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{areChildrenVisible && (
|
||||||
|
<>
|
||||||
|
{!disableEditing && showAddSegment && (
|
||||||
|
<AddSegmentRow
|
||||||
|
onClick={() => onSegmentAdd(segmentation.id)}
|
||||||
|
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||||
|
segmentation={segmentation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={classNames('ohif-scrollbar flex min-h-0 flex-col overflow-y-hidden', {
|
||||||
|
'mt-1': disableEditing || !showAddSegment,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{segmentation?.segments?.map(segment => {
|
||||||
|
if (!segment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { segmentIndex, color, label, isVisible, isLocked, displayText } = segment;
|
||||||
|
return (
|
||||||
|
<div key={segmentIndex}>
|
||||||
|
<SegmentationGroupSegment
|
||||||
|
segmentationId={segmentation.id}
|
||||||
|
segmentIndex={segmentIndex}
|
||||||
|
label={label}
|
||||||
|
color={color}
|
||||||
|
isActive={
|
||||||
|
segmentation.activeSegmentIndex === segmentIndex &&
|
||||||
|
segmentation.id === activeSegmentationId
|
||||||
|
}
|
||||||
|
disableEditing={disableEditing}
|
||||||
|
isLocked={isLocked}
|
||||||
|
isVisible={isVisible}
|
||||||
|
onClick={onSegmentClick}
|
||||||
|
onEdit={onSegmentEdit}
|
||||||
|
onDelete={onSegmentDelete}
|
||||||
|
showDelete={showDeleteSegment}
|
||||||
|
onColor={onSegmentColorClick}
|
||||||
|
onToggleVisibility={onToggleSegmentVisibility}
|
||||||
|
onToggleLocked={onToggleSegmentLock}
|
||||||
|
displayText={displayText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SegmentationItem.propTypes = {
|
||||||
|
segmentation: PropTypes.object,
|
||||||
|
disableEditing: PropTypes.bool,
|
||||||
|
onToggleSegmentationVisibility: PropTypes.func,
|
||||||
|
onSegmentationEdit: PropTypes.func,
|
||||||
|
onSegmentationDownload: PropTypes.func,
|
||||||
|
onSegmentationDownloadRTSS: PropTypes.func,
|
||||||
|
storeSegmentation: PropTypes.func,
|
||||||
|
onSegmentationDelete: PropTypes.func,
|
||||||
|
showAddSegment: PropTypes.bool,
|
||||||
|
onSegmentAdd: PropTypes.func,
|
||||||
|
onSegmentClick: PropTypes.func,
|
||||||
|
onSegmentDelete: PropTypes.func,
|
||||||
|
onSegmentEdit: PropTypes.func,
|
||||||
|
showDeleteSegment: PropTypes.bool,
|
||||||
|
onSegmentColorClick: PropTypes.func,
|
||||||
|
onToggleSegmentVisibility: PropTypes.func,
|
||||||
|
onToggleSegmentLock: PropTypes.func,
|
||||||
|
activeSegmentationId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
SegmentationItem.defaultProps = {
|
||||||
|
segmentation: null,
|
||||||
|
disableEditing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SegmentationItem;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
import SegmentationGroupTable from './SegmentationGroupTable';
|
import SegmentationGroupTable from './SegmentationGroupTable';
|
||||||
|
import SegmentationGroupTableExpanded from './SegmentationGroupTableExpanded';
|
||||||
|
|
||||||
export default SegmentationGroupTable;
|
export { SegmentationGroupTable, SegmentationGroupTableExpanded };
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const ToolbarButton = ({
|
|||||||
//
|
//
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
|
disabledText,
|
||||||
size,
|
size,
|
||||||
toolTipClassName,
|
toolTipClassName,
|
||||||
disableToolTip = false,
|
disableToolTip = false,
|
||||||
@ -38,7 +39,7 @@ const ToolbarButton = ({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
isSticky={shouldShowDropdown}
|
isSticky={shouldShowDropdown}
|
||||||
content={shouldShowDropdown ? dropdownContent : label}
|
content={shouldShowDropdown ? dropdownContent : label}
|
||||||
secondaryContent={disabled ? 'Not available on the current viewport' : null}
|
secondaryContent={disabledText}
|
||||||
tight={shouldShowDropdown}
|
tight={shouldShowDropdown}
|
||||||
className={toolTipClassNameToUse}
|
className={toolTipClassNameToUse}
|
||||||
isDisabled={disableToolTip}
|
isDisabled={disableToolTip}
|
||||||
|
|||||||
@ -43,8 +43,14 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
|
|||||||
(accumulator, toolbarButton) => {
|
(accumulator, toolbarButton) => {
|
||||||
const { id: buttonId, componentProps } = toolbarButton;
|
const { id: buttonId, componentProps } = toolbarButton;
|
||||||
|
|
||||||
const createEnhancedOptions = (options, parentId) =>
|
const createEnhancedOptions = (options, parentId) => {
|
||||||
options.map(option => {
|
const optionsToUse = Array.isArray(options) ? options : [options];
|
||||||
|
|
||||||
|
return optionsToUse.map(option => {
|
||||||
|
if (typeof option.optionComponent === 'function') {
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
value:
|
value:
|
||||||
@ -72,13 +78,18 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (componentProps.items?.length) {
|
const { items, options } = componentProps;
|
||||||
componentProps.items.forEach(item => {
|
|
||||||
accumulator[item.id] = createEnhancedOptions(item.options, item.id);
|
if (items?.length) {
|
||||||
|
items.forEach(({ options, id }) => {
|
||||||
|
accumulator[id] = createEnhancedOptions(options, id);
|
||||||
});
|
});
|
||||||
} else if (componentProps.options?.length) {
|
} else if (options?.length) {
|
||||||
accumulator[buttonId] = createEnhancedOptions(componentProps.options, buttonId);
|
accumulator[buttonId] = createEnhancedOptions(options, buttonId);
|
||||||
|
} else if (options?.optionComponent) {
|
||||||
|
accumulator[buttonId] = options.optionComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accumulator;
|
return accumulator;
|
||||||
|
|||||||
@ -53,8 +53,9 @@ function ToolboxUI(props) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
position="bottom"
|
position="bottom"
|
||||||
content={componentProps.label}
|
content={componentProps.label}
|
||||||
secondaryContent={'Not available on the current viewport'}
|
secondaryContent={componentProps.disabledText}
|
||||||
>
|
>
|
||||||
|
<div className="border-secondary-light rounded border bg-black">
|
||||||
<Component
|
<Component
|
||||||
{...componentProps}
|
{...componentProps}
|
||||||
{...props}
|
{...props}
|
||||||
@ -63,8 +64,10 @@ function ToolboxUI(props) {
|
|||||||
onInteraction={onInteraction}
|
onInteraction={onInteraction}
|
||||||
size="toolbox"
|
size="toolbox"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="border-secondary-light rounded border bg-black">
|
||||||
<Component
|
<Component
|
||||||
{...componentProps}
|
{...componentProps}
|
||||||
{...props}
|
{...props}
|
||||||
@ -73,6 +76,7 @@ function ToolboxUI(props) {
|
|||||||
onInteraction={onInteraction}
|
onInteraction={onInteraction}
|
||||||
size="toolbox"
|
size="toolbox"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import './tooltip.css';
|
import './tooltip.css';
|
||||||
|
|
||||||
@ -42,6 +43,10 @@ const Tooltip = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
const { t } = useTranslation('Buttons');
|
const { t } = useTranslation('Buttons');
|
||||||
|
const tooltipContainer = document.getElementById('react-portal');
|
||||||
|
const [coords, setCoords] = useState({ x: 999999, y: 999999 });
|
||||||
|
const parentRef = useRef(null);
|
||||||
|
const tooltipRef = useRef(null);
|
||||||
|
|
||||||
const handleMouseOver = () => {
|
const handleMouseOver = () => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
@ -57,23 +62,61 @@ const Tooltip = ({
|
|||||||
|
|
||||||
const isOpen = (isSticky || isActive) && !isDisabled;
|
const isOpen = (isSticky || isActive) && !isDisabled;
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
|
if (parentRef.current && tooltipRef.current) {
|
||||||
|
const parentRect = parentRef.current.getBoundingClientRect();
|
||||||
|
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||||
|
const parentWidth = parentRect.width;
|
||||||
|
const parentHeight = parentRect.height;
|
||||||
|
const tooltipWidth = tooltipRect.width;
|
||||||
|
|
||||||
|
let newX = 0;
|
||||||
|
let newY = 0;
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case 'bottom':
|
||||||
|
newX = parentRect.left + parentWidth / 2;
|
||||||
|
newY = parentRect.top + parentHeight;
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
newX = parentRect.left + parentWidth / 2;
|
||||||
|
newY = parentRect.top - parentHeight * 2;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
newX = parentRect.left + parentWidth;
|
||||||
|
newY = parentRect.top + parentHeight / 2;
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
newX = parentRect.left - tooltipWidth - 10;
|
||||||
|
newY = parentRect.top + parentHeight / 2;
|
||||||
|
break;
|
||||||
|
case 'bottom-left':
|
||||||
|
newX = parentRect.left;
|
||||||
|
newY = parentRect.top + parentHeight;
|
||||||
|
break;
|
||||||
|
case 'bottom-right':
|
||||||
|
newX = parentRect.left - tooltipWidth + parentWidth;
|
||||||
|
newY = parentRect.top + parentHeight;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCoords({ x: newX, y: newY });
|
||||||
|
}
|
||||||
|
}, [isOpen, position, parentRef.current, tooltipRef.current]);
|
||||||
|
|
||||||
|
const tooltipContent = (
|
||||||
<div
|
<div
|
||||||
className={classnames('relative', className)}
|
className={classnames(`tooltip tooltip-${position} block`, 'z-50')}
|
||||||
onMouseOver={handleMouseOver}
|
style={{
|
||||||
onFocus={handleMouseOver}
|
position: 'fixed',
|
||||||
onMouseOut={handleMouseOut}
|
top: coords.y,
|
||||||
onBlur={handleMouseOut}
|
left: isOpen ? coords.x : 999999,
|
||||||
role="tooltip"
|
}}
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<div
|
|
||||||
className={classnames(`tooltip tooltip-${position}`, {
|
|
||||||
block: isOpen,
|
|
||||||
hidden: !isOpen,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
|
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
|
||||||
{
|
{
|
||||||
@ -98,6 +141,20 @@ const Tooltip = ({
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={parentRef}
|
||||||
|
className={classnames('relative', className)}
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onFocus={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
onBlur={handleMouseOut}
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{tooltipContainer && ReactDOM.createPortal(tooltipContent, tooltipContainer)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -110,7 +167,6 @@ Tooltip.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
Tooltip.propTypes = {
|
||||||
/** prevents tooltip from rendering despite hover/active/sticky */
|
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||||
position: PropTypes.oneOf(['bottom', 'bottom-left', 'bottom-right', 'left', 'right', 'top']),
|
position: PropTypes.oneOf(['bottom', 'bottom-left', 'bottom-right', 'left', 'right', 'top']),
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import NavBar from './NavBar';
|
|||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
import SegmentationTable from './SegmentationTable';
|
import SegmentationTable from './SegmentationTable';
|
||||||
import SegmentationGroupTable from './SegmentationGroupTable';
|
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from './SegmentationGroupTable';
|
||||||
import SidePanel from './SidePanel';
|
import SidePanel from './SidePanel';
|
||||||
import SplitButton from './SplitButton';
|
import SplitButton from './SplitButton';
|
||||||
import StudyBrowser from './StudyBrowser';
|
import StudyBrowser from './StudyBrowser';
|
||||||
@ -144,6 +144,7 @@ export {
|
|||||||
Select,
|
Select,
|
||||||
SegmentationTable,
|
SegmentationTable,
|
||||||
SegmentationGroupTable,
|
SegmentationGroupTable,
|
||||||
|
SegmentationGroupTableExpanded,
|
||||||
SidePanel,
|
SidePanel,
|
||||||
SplitButton,
|
SplitButton,
|
||||||
StudyBrowser,
|
StudyBrowser,
|
||||||
|
|||||||
@ -81,6 +81,7 @@ export {
|
|||||||
Select,
|
Select,
|
||||||
SegmentationTable,
|
SegmentationTable,
|
||||||
SegmentationGroupTable,
|
SegmentationGroupTable,
|
||||||
|
SegmentationGroupTableExpanded,
|
||||||
SidePanel,
|
SidePanel,
|
||||||
SplitButton,
|
SplitButton,
|
||||||
LegacySplitButton,
|
LegacySplitButton,
|
||||||
|
|||||||
@ -2420,7 +2420,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3"
|
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3"
|
||||||
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
|
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
|
||||||
|
|
||||||
"@emotion/use-insertion-effect-with-fallbacks@^1.0.1":
|
"@emotion/use-insertion-effect-with-fallbacks@^1.0.0", "@emotion/use-insertion-effect-with-fallbacks@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963"
|
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963"
|
||||||
integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==
|
integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user