feat(toolbox): Refactor Toolbar and Toolbox to enable sections (#4825)
This commit is contained in:
1 parent
86c12246ac
commit
cc5cdfb081
79 files changed
+3527
-2992
No files matched your search
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.0.1",
|
||||
"@cornerstonejs/core": "^3.0.1",
|
||||
"@cornerstonejs/adapters": "^3.0.4",
|
||||
"@cornerstonejs/core": "^3.0.4",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.0.1",
|
||||
"@cornerstonejs/core": "^3.0.1",
|
||||
"@cornerstonejs/adapters": "^3.0.4",
|
||||
"@cornerstonejs/core": "^3.0.4",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@@ -329,9 +329,19 @@ const commandsModule = ({
|
||||
},
|
||||
setThresholdRange: ({
|
||||
value,
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
toolNames = [
|
||||
'ThresholdCircularBrush',
|
||||
'ThresholdSphereBrush',
|
||||
'ThresholdCircularBrushDynamic',
|
||||
'ThresholdSphereBrushDynamic',
|
||||
],
|
||||
}) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
if (!toolGroupIds?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolNames?.forEach(toolName => {
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
@@ -340,7 +350,7 @@ const commandsModule = ({
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.0.1",
|
||||
"@cornerstonejs/core": "^3.0.1",
|
||||
"@cornerstonejs/tools": "^3.0.1",
|
||||
"@cornerstonejs/adapters": "^3.0.4",
|
||||
"@cornerstonejs/core": "^3.0.4",
|
||||
"@cornerstonejs/tools": "^3.0.4",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^3.0.1",
|
||||
"@cornerstonejs/tools": "^3.0.1",
|
||||
"@cornerstonejs/core": "^3.0.4",
|
||||
"@cornerstonejs/tools": "^3.0.4",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { Toolbox } from '@ohif/extension-default';
|
||||
import { PanelSegmentation } from '@ohif/extension-cornerstone';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
@@ -19,9 +19,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager, co
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.0.1",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.0.4",
|
||||
"@ohif/core": "3.10.0-beta.116",
|
||||
"@ohif/ui": "3.10.0-beta.116",
|
||||
"dcmjs": "*",
|
||||
@@ -51,10 +51,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.0.1",
|
||||
"@cornerstonejs/core": "^3.0.1",
|
||||
"@cornerstonejs/adapters": "^3.0.4",
|
||||
"@cornerstonejs/core": "^3.0.4",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.0.1",
|
||||
"@cornerstonejs/tools": "^3.0.1",
|
||||
"@cornerstonejs/tools": "^3.0.4",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@@ -388,7 +388,12 @@ function commandsModule({
|
||||
arrowTextCallback: ({ callback }) => {
|
||||
const labelConfig = customizationService.getCustomization('measurementLabels');
|
||||
const renderContent = customizationService.getCustomization('ui.labellingComponent');
|
||||
callInputDialogAutoComplete(uiDialogService, callback, {}, labelConfig, renderContent);
|
||||
|
||||
callInputDialogAutoComplete({
|
||||
uiDialogService,
|
||||
labelConfig,
|
||||
renderContent,
|
||||
});
|
||||
},
|
||||
toggleCine: () => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
-1
@@ -23,7 +23,6 @@ export function getWindowLevelActionMenu({
|
||||
const WindowLevelActionMenu = customizationService.getCustomization(
|
||||
'viewportActionMenu.windowLevelActionMenu'
|
||||
);
|
||||
console.debug('🚀 ~ WindowLevelActionMenu:', WindowLevelActionMenu);
|
||||
const displaySetPresets = displaySets
|
||||
.filter(displaySet => presets[displaySet.Modality])
|
||||
.map(displaySet => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { Toolbox } from '@ohif/extension-default';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
import PanelMeasurement from './panels/PanelMeasurement';
|
||||
@@ -36,14 +36,8 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="segmentationToolbox"
|
||||
title="Segmentation Tools"
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
/>
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
|
||||
@@ -87,61 +87,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
isActive: isPrimaryActive,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
evaluate: ({ viewportId, button, itemId }) => {
|
||||
const { items } = button.props;
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return {
|
||||
primary: button.props.primary,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
||||
|
||||
// check if the active toolName is part of the items then we need
|
||||
// to move it to the primary button
|
||||
const activeToolIndex = items.findIndex(item => {
|
||||
const toolName = toolbarService.getToolNameForButton(item);
|
||||
return toolName === activeToolName;
|
||||
});
|
||||
|
||||
// if there is an active tool in the items dropdown bound to the primary mouse/touch
|
||||
// we should show that no matter what
|
||||
if (activeToolIndex > -1) {
|
||||
return {
|
||||
primary: items[activeToolIndex],
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
if (!itemId) {
|
||||
return {
|
||||
primary: button.props.primary,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
// other wise we can move the clicked tool to the primary button
|
||||
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
|
||||
|
||||
return {
|
||||
primary: clickedItemProps,
|
||||
items,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.action',
|
||||
evaluate: () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { roundNumber } from '@ohif/core/src/utils';
|
||||
import { SegmentationData } from '../services/SegmentationService/SegmentationService';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
const { label, segments } = segmentation;
|
||||
@@ -83,11 +84,12 @@ function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
* @param options.debounceTime - Debounce time in milliseconds for updates.
|
||||
* @returns An array of segmentation data.
|
||||
*/
|
||||
export function useSegmentations({
|
||||
servicesManager,
|
||||
subscribeToDataModified = false,
|
||||
debounceTime = 0,
|
||||
}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] {
|
||||
export function useSegmentations(options: {
|
||||
subscribeToDataModified?: boolean;
|
||||
debounceTime?: number;
|
||||
}): SegmentationData[] {
|
||||
const { subscribeToDataModified = false, debounceTime = 0 } = options || {};
|
||||
const { servicesManager } = useSystem();
|
||||
const { segmentationService, customizationService } = servicesManager.services;
|
||||
|
||||
const [segmentations, setSegmentations] = useState<SegmentationData[]>([]);
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ToolButton } from '@ohif/ui-next';
|
||||
import { useToolbar } from '@ohif/core/src/hooks/useToolbar';
|
||||
|
||||
/**
|
||||
* Wraps the ToolButtonList component to handle the OHIF toolbar button structure
|
||||
* @param props - Component props
|
||||
* @returns Component
|
||||
*/
|
||||
export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...props }) {
|
||||
if (!items || !groupId) {
|
||||
export function ToolBoxButtonGroupWrapper({ groupId, buttonSection, ...props }) {
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
buttonSection,
|
||||
});
|
||||
|
||||
if (!groupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = toolbarButtons.map(button => button.componentProps);
|
||||
|
||||
return (
|
||||
<div className="bg-popover flex flex-row space-x-1 rounded-md px-0 py-0">
|
||||
{items.map(item => (
|
||||
@@ -20,16 +27,22 @@ export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...pr
|
||||
key={item.id}
|
||||
size="small"
|
||||
className={props.disabled && 'text-primary'}
|
||||
onInteraction={() =>
|
||||
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
|
||||
}
|
||||
onInteraction={event => {
|
||||
onInteraction?.({
|
||||
event,
|
||||
groupId,
|
||||
commands: item.commands,
|
||||
itemId: item.id,
|
||||
item,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolBoxButtonWrapper({ onInteraction, ...props }) {
|
||||
export function ToolBoxButtonWrapper({ onInteraction, options, ...props }) {
|
||||
return (
|
||||
<div className="bg-popover flex flex-row rounded-md px-0 py-0">
|
||||
<ToolButton
|
||||
@@ -37,7 +50,14 @@ export function ToolBoxButtonWrapper({ onInteraction, ...props }) {
|
||||
id={props.id}
|
||||
size="small"
|
||||
className={classNames(props.disabled && 'text-primary')}
|
||||
onInteraction={() => onInteraction?.({ itemId: props.id, commands: props.commands })}
|
||||
onInteraction={event => {
|
||||
onInteraction?.({
|
||||
event,
|
||||
itemId: props.id,
|
||||
commands: props.commands,
|
||||
options,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,23 +7,11 @@ import {
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
interface ButtonItem {
|
||||
id: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
isActive?: boolean;
|
||||
disabledText?: string;
|
||||
commands?: Record<string, unknown>;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
import { useToolbar } from '@ohif/core/src';
|
||||
|
||||
interface ToolButtonListWrapperProps {
|
||||
groupId: string;
|
||||
primary: ButtonItem;
|
||||
items: ButtonItem[];
|
||||
buttonSection: string;
|
||||
onInteraction?: (details: {
|
||||
groupId: string;
|
||||
itemId: string;
|
||||
@@ -39,10 +27,22 @@ interface ToolButtonListWrapperProps {
|
||||
*/
|
||||
export default function ToolButtonListWrapper({
|
||||
groupId,
|
||||
primary,
|
||||
items,
|
||||
onInteraction,
|
||||
buttonSection,
|
||||
}: ToolButtonListWrapperProps) {
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
buttonSection,
|
||||
});
|
||||
|
||||
if (!toolbarButtons?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primary =
|
||||
toolbarButtons.find(button => button.componentProps.isActive)?.componentProps ||
|
||||
toolbarButtons[0].componentProps;
|
||||
|
||||
const items = toolbarButtons.map(button => button.componentProps);
|
||||
|
||||
return (
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
@@ -63,20 +63,22 @@ export default function ToolButtonListWrapper({
|
||||
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
|
||||
<div data-cy={`${groupId}-split-button-secondary`}>
|
||||
<ToolButtonListDropDown>
|
||||
{items.map(item => (
|
||||
<ToolButtonListItem
|
||||
key={item.id}
|
||||
{...item}
|
||||
data-cy={item.id}
|
||||
data-tool={item.id}
|
||||
data-active={item.isActive}
|
||||
onSelect={() =>
|
||||
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
|
||||
}
|
||||
>
|
||||
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
|
||||
</ToolButtonListItem>
|
||||
))}
|
||||
{items.map(item => {
|
||||
return (
|
||||
<ToolButtonListItem
|
||||
key={item.id}
|
||||
{...item}
|
||||
data-cy={item.id}
|
||||
data-tool={item.id}
|
||||
data-active={item.isActive}
|
||||
onSelect={() =>
|
||||
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
|
||||
}
|
||||
>
|
||||
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
|
||||
</ToolButtonListItem>
|
||||
);
|
||||
})}
|
||||
</ToolButtonListDropDown>
|
||||
</div>
|
||||
</ToolButtonList>
|
||||
|
||||
@@ -19,6 +19,7 @@ function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size })
|
||||
groupId,
|
||||
itemId: item.id,
|
||||
commands: item.commands,
|
||||
item,
|
||||
});
|
||||
}}
|
||||
// Note: this is necessary since tooltip will add
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToolbarSplitButtonWithServices({
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: item.id,
|
||||
commands: item.commands,
|
||||
item,
|
||||
});
|
||||
},
|
||||
})),
|
||||
|
||||
@@ -16,6 +16,10 @@ import { panelGroupDefinition } from './constants/panels';
|
||||
* @param width the max and min width to set on the element
|
||||
*/
|
||||
const setMinMaxWidth = (elem, width?) => {
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem.style.minWidth = width === undefined ? '' : `${width}px`;
|
||||
elem.style.maxWidth = elem.style.minWidth;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,9 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
dataSources?.[0]?.value ?? null
|
||||
);
|
||||
|
||||
const handleSave = (reportName: string) => {
|
||||
const [reportName, setReportName] = useState('');
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
@@ -62,7 +64,11 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
</div>
|
||||
)}
|
||||
<div className={showDataSourceSelect ? 'mt-1 w-2/3' : 'w-full'}>
|
||||
<InputDialog>
|
||||
<InputDialog
|
||||
value={reportName}
|
||||
onChange={setReportName}
|
||||
submitOnEnter
|
||||
>
|
||||
<InputDialog.Field>
|
||||
<InputDialog.Input placeholder="Report name" />
|
||||
</InputDialog.Field>
|
||||
|
||||
@@ -60,27 +60,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
name: 'ohif.progressDropdown',
|
||||
defaultComponent: ProgressDropdownWithService,
|
||||
},
|
||||
{
|
||||
name: 'evaluate.group.promoteToPrimary',
|
||||
evaluate: ({ viewportId, button, itemId }) => {
|
||||
const { items } = button.props;
|
||||
|
||||
if (!itemId) {
|
||||
return {
|
||||
primary: button.props.primary,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
// other wise we can move the clicked tool to the primary button
|
||||
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
|
||||
|
||||
return {
|
||||
primary: clickedItemProps,
|
||||
items,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cine',
|
||||
evaluate: () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import promptLabelAnnotation from './utils/promptLabelAnnotation';
|
||||
import usePatientInfo from './hooks/usePatientInfo';
|
||||
import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowserHeader';
|
||||
import * as utils from './utils';
|
||||
import { Toolbox } from './utils';
|
||||
import MoreDropdownMenu from './Components/MoreDropdownMenu';
|
||||
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
||||
const defaultExtension: Types.Extensions.Extension = {
|
||||
@@ -97,6 +98,7 @@ export {
|
||||
usePatientInfo,
|
||||
PanelStudyBrowserHeader,
|
||||
utils,
|
||||
Toolbox,
|
||||
MoreDropdownMenu,
|
||||
requestDisplaySetCreationForStudy,
|
||||
callInputDialog,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { PanelSection, ToolSettings } from '@ohif/ui-next';
|
||||
import { useSystem, useToolbar } from '@ohif/core';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface ButtonProps {
|
||||
isActive?: boolean;
|
||||
options?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A toolbox is a collection of buttons and commands that they invoke, used to provide
|
||||
* custom control panels to users. This component is a generic UI component that
|
||||
* interacts with services and commands in a generic fashion. While it might
|
||||
* seem unconventional to import it from the UI and integrate it into the JSX,
|
||||
* it belongs in the UI components as there isn't anything in this component that
|
||||
* couldn't be used for a completely different type of app. It plays a crucial
|
||||
* role in enhancing the app with a toolbox by providing a way to integrate
|
||||
* and display various tools and their corresponding options
|
||||
*/
|
||||
export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; title: string }) {
|
||||
const { servicesManager } = useSystem();
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
const { toolbarButtons: toolboxSections, onInteraction } = useToolbar({
|
||||
servicesManager,
|
||||
buttonSection: buttonSectionId,
|
||||
});
|
||||
|
||||
if (!toolboxSections.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure we have proper button sections at the top level.
|
||||
if (!toolboxSections.every(section => section.componentProps.buttonSection)) {
|
||||
throw new Error(
|
||||
'Toolbox accepts only button sections at the top level, not buttons. Create at least one button section.'
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to check a list of buttons for an active tool.
|
||||
const findActiveOptions = (buttons: any[]): unknown => {
|
||||
for (const tool of buttons) {
|
||||
if (tool.componentProps.isActive) {
|
||||
return tool.componentProps.options;
|
||||
}
|
||||
if (tool.componentProps.buttonSection) {
|
||||
const nestedButtons = toolbarService.getButtonPropsInButtonSection(
|
||||
tool.componentProps.buttonSection
|
||||
) as ButtonProps[];
|
||||
const activeNested = nestedButtons.find(nested => nested.isActive);
|
||||
if (activeNested) {
|
||||
return activeNested.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Look for active tool options across all sections.
|
||||
const activeToolOptions = toolboxSections.reduce((activeOptions, section) => {
|
||||
if (activeOptions) {
|
||||
return activeOptions;
|
||||
}
|
||||
const sectionId = section.componentProps.buttonSection;
|
||||
const buttons = toolbarService.getButtonSection(sectionId);
|
||||
return findActiveOptions(buttons);
|
||||
}, null);
|
||||
|
||||
// Define the interaction handler once.
|
||||
const handleInteraction = ({ itemId }: { itemId: string }) => {
|
||||
onInteraction?.({ itemId });
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
<PanelSection.Header>
|
||||
<span>{title}</span>
|
||||
</PanelSection.Header>
|
||||
<PanelSection.Content className="flex-shrink-0 border-none">
|
||||
{toolboxSections.map(section => {
|
||||
const sectionId = section.componentProps.buttonSection;
|
||||
const buttons = toolbarService.getButtonSection(sectionId) as any[];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={sectionId}
|
||||
className="bg-muted flex flex-wrap space-x-2 py-2 px-1"
|
||||
>
|
||||
{buttons.map(tool => {
|
||||
if (!tool) {
|
||||
return null;
|
||||
}
|
||||
const { id, Component, componentProps } = tool;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={classnames('ml-1')}
|
||||
>
|
||||
<Component
|
||||
{...componentProps}
|
||||
id={id}
|
||||
onInteraction={handleInteraction}
|
||||
size="toolbox"
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{activeToolOptions && (
|
||||
<div className="bg-primary-dark h-auto px-2">
|
||||
<ToolSettings options={activeToolOptions} />
|
||||
</div>
|
||||
)}
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
);
|
||||
}
|
||||
@@ -57,6 +57,10 @@ export async function callInputDialog({
|
||||
submitOnEnter = true,
|
||||
}: {
|
||||
uiDialogService: AppTypes.UIDialogService;
|
||||
defaultValue?: string;
|
||||
title?: string;
|
||||
placeholder?: string;
|
||||
submitOnEnter?: boolean;
|
||||
}) {
|
||||
const dialogId = 'dialog-enter-annotation';
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { addIcon } from './addIcon';
|
||||
export * from './Toolbox';
|
||||
@@ -32,8 +32,8 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^3.0.1",
|
||||
"@cornerstonejs/tools": "^3.0.1",
|
||||
"@cornerstonejs/core": "^3.0.4",
|
||||
"@cornerstonejs/tools": "^3.0.4",
|
||||
"@ohif/core": "3.10.0-beta.116",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.116",
|
||||
"@ohif/extension-default": "3.10.0-beta.116",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useCallback, useReducer, useEffect } from 'react';
|
||||
import React, { useCallback, useReducer } from 'react';
|
||||
import { Button } from '@ohif/ui';
|
||||
import ROIThresholdConfiguration, {
|
||||
ROI_STAT,
|
||||
} from './PanelROIThresholdSegmentation/ROIThresholdConfiguration';
|
||||
import * as cs3dTools from '@cornerstonejs/tools';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { useSegmentations } from '@ohif/extension-cornerstone';
|
||||
|
||||
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
||||
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
||||
@@ -40,9 +42,10 @@ function reducer(state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
function RectangleROIOptions() {
|
||||
const { commandsManager } = useSystem();
|
||||
const segmentations = useSegmentations();
|
||||
const activeSegmentation = segmentations[0];
|
||||
|
||||
const runCommand = useCallback(
|
||||
(commandName, commandOptions = {}) => {
|
||||
@@ -61,58 +64,20 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
|
||||
});
|
||||
|
||||
const handleROIThresholding = useCallback(() => {
|
||||
const segmentationId = selectedSegmentationId;
|
||||
if (!activeSegmentation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentationId = activeSegmentation.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?)
|
||||
runCommand('thresholdSegmentationByRectangleROITool', {
|
||||
segmentationId,
|
||||
config,
|
||||
segmentIndex: activeSegmentIndex,
|
||||
});
|
||||
}, [selectedSegmentationId, config]);
|
||||
|
||||
useEffect(() => {
|
||||
const segmentations = segmentationService.getSegmentationRepresentations();
|
||||
|
||||
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 updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED;
|
||||
const subscriptions = [];
|
||||
|
||||
[updated].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentationRepresentations();
|
||||
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isActive = segmentations.find(seg => seg.isActive);
|
||||
setSelectedSegmentationId(isActive.id);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
}, [activeSegmentation, config]);
|
||||
|
||||
return (
|
||||
<div className="invisible-scrollbar mb-2 flex flex-col overflow-y-auto overflow-x-hidden">
|
||||
@@ -121,7 +86,7 @@ function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes)
|
||||
dispatch={dispatch}
|
||||
runCommand={runCommand}
|
||||
/>
|
||||
{selectedSegmentationId !== null && (
|
||||
{activeSegmentation && (
|
||||
<Button
|
||||
className="mt-2 !h-[26px] !w-[75px]"
|
||||
onClick={handleROIThresholding}
|
||||
|
||||
@@ -102,7 +102,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
// corrected PT vs the non-attenuation correct PT)
|
||||
|
||||
let ptDisplaySet = null;
|
||||
for (const [viewportId, viewportDetails] of viewportMatchDetails) {
|
||||
for (const [, viewportDetails] of viewportMatchDetails) {
|
||||
const { displaySetsInfo } = viewportDetails;
|
||||
const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) =>
|
||||
displaySetService.getDisplaySetByUID(displaySetInstanceUID)
|
||||
@@ -199,15 +199,21 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
|
||||
const { representationData } = segmentation;
|
||||
const { displaySetMatchDetails: matchDetails } = hangingProtocolService.getMatchDetails();
|
||||
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
|
||||
const ctDisplaySetMatch = matchDetails.get('ctDisplaySet');
|
||||
const ptDisplaySetMatch = matchDetails.get('ptDisplaySet');
|
||||
|
||||
const ctDisplaySet = matchDetails.get('ctDisplaySet');
|
||||
const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
|
||||
const ctDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
ctDisplaySetMatch.displaySetInstanceUID
|
||||
);
|
||||
const ptDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
ptDisplaySetMatch.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const { volumeId: segVolumeId } = representationData[
|
||||
SegmentationRepresentations.Labelmap
|
||||
] as csTools.Types.LabelmapToolOperationDataVolume;
|
||||
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
|
||||
|
||||
const labelmapVolume = cs.cache.getVolume(segVolumeId);
|
||||
|
||||
const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS);
|
||||
|
||||
@@ -220,71 +226,40 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
return;
|
||||
}
|
||||
|
||||
const labelmapVolume = cs.cache.getVolume(segmentationId);
|
||||
let referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
|
||||
|
||||
// check if viewport is
|
||||
|
||||
if (!referencedVolume) {
|
||||
throw new Error('No Reference volume found');
|
||||
}
|
||||
|
||||
if (!labelmapVolume) {
|
||||
throw new Error('No Reference labelmap found');
|
||||
}
|
||||
|
||||
const annotation = csTools.annotation.state.getAnnotation(annotationUIDs[0]);
|
||||
|
||||
const {
|
||||
metadata: {
|
||||
enabledElement: { viewport },
|
||||
},
|
||||
} = annotation;
|
||||
|
||||
const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId);
|
||||
|
||||
if (!showingReferenceVolume) {
|
||||
// if the reference volume is not being displayed, we can't
|
||||
// rely on it for thresholding, we have couple of options here
|
||||
// 1. We choose whatever volume is being displayed
|
||||
// 2. We check if it is a fusion viewport, we pick the volume
|
||||
// that matches the size and dimensions of the labelmap. This might
|
||||
// happen if the 4D PT is converted to a computed volume and displayed
|
||||
// and wants to threshold the labelmap
|
||||
// 3. We throw an error
|
||||
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
||||
viewport.id
|
||||
);
|
||||
|
||||
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||
const volume = cs.cache
|
||||
.getVolumes()
|
||||
.find(volume => volume.volumeId.includes(displaySetInstanceUID));
|
||||
|
||||
if (
|
||||
cs.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) &&
|
||||
cs.utilities.isEqual(volume.spacing, labelmapVolume.spacing)
|
||||
) {
|
||||
referencedVolume = volume;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
|
||||
annotationUIDs,
|
||||
[referencedVolume, ctReferencedVolume],
|
||||
ptDisplaySet,
|
||||
config
|
||||
);
|
||||
|
||||
const { imageIds: ptImageIds } = ptDisplaySet;
|
||||
|
||||
const ptVolumeInfo = cs.cache.getVolumeContainingImageId(ptImageIds[0]);
|
||||
|
||||
if (!ptVolumeInfo) {
|
||||
uiNotificationService.error('No PT volume found');
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageIds: ctImageIds } = ctDisplaySet;
|
||||
const ctVolumeInfo = cs.cache.getVolumeContainingImageId(ctImageIds[0]);
|
||||
|
||||
if (!ctVolumeInfo) {
|
||||
uiNotificationService.error('No CT volume found');
|
||||
return;
|
||||
}
|
||||
|
||||
const ptVolume = ptVolumeInfo.volume;
|
||||
const ctVolume = ctVolumeInfo.volume;
|
||||
|
||||
return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange(
|
||||
annotationUIDs,
|
||||
labelmapVolume,
|
||||
[
|
||||
{ volume: referencedVolume, lower: ptLower, upper: ptUpper },
|
||||
{ volume: ctReferencedVolume, lower: ctLower, upper: ctUpper },
|
||||
{ volume: ptVolume, lower: ptLower, upper: ptUpper },
|
||||
{ volume: ctVolume, lower: ctLower, upper: ctUpper },
|
||||
],
|
||||
{ overwrite: true, segmentIndex }
|
||||
{ overwrite: true, segmentIndex, segmentationId }
|
||||
);
|
||||
},
|
||||
calculateSuvPeak: async ({ segmentationId, segmentIndex }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { Toolbox } from '@ohif/extension-default';
|
||||
import PanelTMTV from './Panels/PanelTMTV';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
@@ -17,9 +17,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
const wrappedROIThresholdToolbox = () => {
|
||||
return (
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="ROIThresholdToolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
@@ -39,9 +36,6 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="ROIThresholdToolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import RectangleROIOptions from './Panels/RectangleROIOptions';
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
export default function getToolbarModule() {
|
||||
return [
|
||||
{
|
||||
name: 'tmtv.RectangleROIThresholdOptions',
|
||||
defaultComponent: () => RectangleROIOptions({ commandsManager, servicesManager }),
|
||||
defaultComponent: RectangleROIOptions,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
|
||||
function getRoiStats(referencedVolume, annotations) {
|
||||
// roiStats
|
||||
const { imageData } = referencedVolume;
|
||||
const values = imageData.getPointData().getScalars().getData();
|
||||
function getRoiStats(displaySet, annotations) {
|
||||
const { imageIds } = displaySet;
|
||||
|
||||
const ptVolumeInfo = cache.getVolumeContainingImageId(imageIds[0]);
|
||||
|
||||
if (!ptVolumeInfo) {
|
||||
throw new Error('No volume found for display set');
|
||||
}
|
||||
|
||||
const { volume } = ptVolumeInfo;
|
||||
const { voxelManager } = volume;
|
||||
|
||||
// Todo: add support for other strategies
|
||||
const { fn, baseValue } = _getStrategyFn('max');
|
||||
@@ -11,25 +19,25 @@ function getRoiStats(referencedVolume, annotations) {
|
||||
|
||||
const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(
|
||||
annotations,
|
||||
referencedVolume
|
||||
volume
|
||||
);
|
||||
|
||||
const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK;
|
||||
|
||||
for (let i = iMin; i <= iMax; i++) {
|
||||
for (let j = jMin; j <= jMax; j++) {
|
||||
for (let k = kMin; k <= kMax; k++) {
|
||||
const offset = imageData.computeOffsetIndex([i, j, k]);
|
||||
value = fn(values[offset], value);
|
||||
}
|
||||
// Use the voxelManager's forEach method to iterate over the bounds
|
||||
voxelManager.forEach(
|
||||
({ value: voxelValue }) => {
|
||||
value = fn(voxelValue, value);
|
||||
},
|
||||
{
|
||||
boundsIJK,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getThresholdValues(
|
||||
annotationUIDs,
|
||||
referencedVolumes,
|
||||
ptDisplaySet,
|
||||
config
|
||||
): { ptLower: number; ptUpper: number; ctLower: number; ctUpper: number } {
|
||||
if (config.strategy === 'range') {
|
||||
@@ -46,7 +54,7 @@ function getThresholdValues(
|
||||
csTools.annotation.state.getAnnotation(annotationUID)
|
||||
);
|
||||
|
||||
const ptValue = getRoiStats(referencedVolumes[0], annotations);
|
||||
const ptValue = getRoiStats(ptDisplaySet, annotations);
|
||||
|
||||
return {
|
||||
ctLower: -Infinity,
|
||||
|
||||
Reference in new issue
Block a user