feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)
This commit is contained in:
parent
79d5c36bda
commit
566b25a544
@ -13,7 +13,7 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@3.2.1
|
||||
cypress: cypress-io/cypress@3.3.1
|
||||
|
||||
executors:
|
||||
cypress-custom:
|
||||
|
||||
22
.github/workflows/codespell.yml
vendored
22
.github/workflows/codespell.yml
vendored
@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
@ -5,6 +5,7 @@ import { cache, metaData } from '@cornerstonejs/core';
|
||||
import {
|
||||
segmentation as cornerstoneToolsSegmentation,
|
||||
Enums as cornerstoneToolsEnums,
|
||||
utilities,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
@ -18,6 +19,7 @@ import {
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
} from './utils/hydrationUtils';
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
@ -45,6 +47,7 @@ const commandsModule = ({
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
|
||||
const actions = {
|
||||
@ -397,6 +400,60 @@ const commandsModule = ({
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
setBrushSize: ({ value, toolNames }) => {
|
||||
const brushSize = Number(value);
|
||||
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
if (toolNames?.length === 0) {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
|
||||
} else {
|
||||
toolNames?.forEach(toolName => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setThresholdRange: ({
|
||||
value,
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
}) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolNames?.forEach(toolName => {
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleThresholdRangeAndDynamic() {
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
|
||||
if (!toolGroupIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id);
|
||||
|
||||
brushInstances.forEach(({ configuration }) => {
|
||||
const { activeStrategy, strategySpecificConfiguration } = configuration;
|
||||
|
||||
if (activeStrategy.startsWith('THRESHOLD')) {
|
||||
const thresholdConfig = strategySpecificConfiguration.THRESHOLD;
|
||||
|
||||
if (thresholdConfig) {
|
||||
thresholdConfig.isDynamic = !thresholdConfig.isDynamic;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -424,11 +481,21 @@ const commandsModule = ({
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
setBrushSize: {
|
||||
commandFn: actions.setBrushSize,
|
||||
},
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
toggleThresholdRangeAndDynamic: {
|
||||
commandFn: actions.toggleThresholdRangeAndDynamic,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'SEGMENTATION',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import SegmentationToolbox from './panels/SegmentationToolbox';
|
||||
|
||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => {
|
||||
const getPanelModule = ({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
title,
|
||||
}) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = configuration => {
|
||||
@ -26,13 +32,14 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationWithTools = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
return (
|
||||
<>
|
||||
<SegmentationToolbox
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="segmentationToolbox"
|
||||
title="Segmentation Tools"
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
|
||||
65
extensions/cornerstone-dicom-seg/src/getToolbarModule.ts
Normal file
65
extensions/cornerstone-dicom-seg/src/getToolbarModule.ts
Normal file
@ -0,0 +1,65 @@
|
||||
export function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const { segmentationService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
evaluate: ({ viewportId, button, toolNames }) => {
|
||||
// Todo: we need to pass in the button section Id since we are kind of
|
||||
// forcing the button to have black background since initially
|
||||
// it is designed for the toolbox not the toolbar on top
|
||||
// we should then branch the buttonSectionId to have different styles
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright !bg-black opacity-50',
|
||||
};
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
};
|
||||
}
|
||||
|
||||
const isPrimaryActive = toolNames
|
||||
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
|
||||
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
|
||||
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
|
||||
// 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,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getToolNameForButton(button) {
|
||||
const { props } = button;
|
||||
|
||||
const commands = props?.commands || button.commands;
|
||||
|
||||
if (commands && commands.length) {
|
||||
const command = commands[0];
|
||||
const { commandOptions } = command;
|
||||
const { toolName } = commandOptions || { toolName: props?.id ?? button.id };
|
||||
return toolName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -5,6 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import { getToolbarModule } from './getToolbarModule';
|
||||
import preRegistration from './init';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
@ -29,7 +30,6 @@ const extension = {
|
||||
*/
|
||||
id,
|
||||
preRegistration,
|
||||
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
@ -38,8 +38,8 @@ const extension = {
|
||||
*/
|
||||
getPanelModule,
|
||||
getCommandsModule,
|
||||
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
getToolbarModule,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneSEGViewport
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { addTool, BrushTool } from '@cornerstonejs/tools';
|
||||
|
||||
export default function init({ configuration = {} }): void {
|
||||
export default function init({ servicesManager }): void {
|
||||
addTool(BrushTool);
|
||||
}
|
||||
|
||||
@ -1,425 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState, useReducer } from 'react';
|
||||
import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui';
|
||||
import { Types } from '@ohif/extension-cornerstone';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const TOOL_TYPES = {
|
||||
CIRCULAR_BRUSH: 'CircularBrush',
|
||||
SPHERE_BRUSH: 'SphereBrush',
|
||||
CIRCULAR_ERASER: 'CircularEraser',
|
||||
SPHERE_ERASER: 'SphereEraser',
|
||||
CIRCLE_SHAPE: 'CircleScissor',
|
||||
RECTANGLE_SHAPE: 'RectangleScissor',
|
||||
SPHERE_SHAPE: 'SphereScissor',
|
||||
THRESHOLD_CIRCULAR_BRUSH: 'ThresholdCircularBrush',
|
||||
THRESHOLD_SPHERE_BRUSH: 'ThresholdSphereBrush',
|
||||
};
|
||||
|
||||
const ACTIONS = {
|
||||
SET_TOOL_CONFIG: 'SET_TOOL_CONFIG',
|
||||
SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL',
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
Brush: {
|
||||
brushSize: 15,
|
||||
mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush'
|
||||
},
|
||||
Eraser: {
|
||||
brushSize: 15,
|
||||
mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser'
|
||||
},
|
||||
Shapes: {
|
||||
brushSize: 15,
|
||||
mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor'
|
||||
},
|
||||
ThresholdBrush: {
|
||||
brushSize: 15,
|
||||
thresholdRange: null,
|
||||
},
|
||||
activeTool: null,
|
||||
};
|
||||
|
||||
function toolboxReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case ACTIONS.SET_TOOL_CONFIG:
|
||||
const { tool, config } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[tool]: {
|
||||
...state[tool],
|
||||
...config,
|
||||
},
|
||||
};
|
||||
case ACTIONS.SET_ACTIVE_TOOL:
|
||||
return { ...state, activeTool: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function SegmentationToolbox({ servicesManager, extensionManager }) {
|
||||
const { toolbarService, segmentationService, toolGroupService } =
|
||||
servicesManager.services as Types.CornerstoneServices;
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const [toolsEnabled, setToolsEnabled] = useState(false);
|
||||
const [state, dispatch] = useReducer(toolboxReducer, initialState);
|
||||
|
||||
const updateActiveTool = useCallback(() => {
|
||||
if (!viewports?.size || activeViewportId === undefined) {
|
||||
return;
|
||||
}
|
||||
const viewport = viewports.get(activeViewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_ACTIVE_TOOL,
|
||||
payload: toolGroupService.getActiveToolForViewport(viewport.viewportId),
|
||||
});
|
||||
}, [activeViewportId, viewports, toolGroupService, dispatch]);
|
||||
|
||||
const setToolActive = useCallback(
|
||||
toolName => {
|
||||
initializeThresholdValue(toolName);
|
||||
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName });
|
||||
},
|
||||
[toolbarService, dispatch]
|
||||
);
|
||||
|
||||
/**
|
||||
* sets the tools enabled IF there are segmentations
|
||||
*/
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
];
|
||||
|
||||
const unsubscriptions = [];
|
||||
|
||||
events.forEach(event => {
|
||||
const { unsubscribe } = segmentationService.subscribe(event, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
const activeSegmentation = segmentations?.find(seg => seg.isActive);
|
||||
|
||||
setToolsEnabled(activeSegmentation?.segmentCount > 0);
|
||||
});
|
||||
|
||||
unsubscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
updateActiveTool();
|
||||
|
||||
return () => {
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
};
|
||||
}, [activeViewportId, viewports, segmentationService, updateActiveTool]);
|
||||
|
||||
/**
|
||||
* Update the active tool when the toolbar state changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
updateActiveTool();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService, updateActiveTool]);
|
||||
|
||||
useEffect(() => {
|
||||
// if the active tool is not a brush tool then do nothing
|
||||
if (!Object.values(TOOL_TYPES).includes(state.activeTool)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the tool is Segmentation and it is enabled then do nothing
|
||||
if (toolsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the tool is Segmentation and it is disabled, then switch
|
||||
// back to the window level tool to not confuse the user when no
|
||||
// segmentation is active or when there is no segment in the segmentation
|
||||
setToolActive('WindowLevel');
|
||||
}, [toolsEnabled, state.activeTool, setToolActive]);
|
||||
|
||||
const updateBrushSize = useCallback(
|
||||
(toolName, brushSize) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
},
|
||||
[toolGroupService]
|
||||
);
|
||||
|
||||
function initializeThresholdValue(toolName: any) {
|
||||
if (state.ThresholdBrush.thresholdRange === null) {
|
||||
// set the default threshold range from the tool configuration
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
const toolGroupId = toolGroupIds[0];
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const toolConfig = toolGroup.getToolConfiguration(toolName);
|
||||
const defaultThresholdRange = toolConfig?.strategySpecificConfiguration?.THRESHOLD?.threshold;
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: 'ThresholdBrush',
|
||||
config: { thresholdRange: defaultThresholdRange },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const onBrushSizeChange = useCallback(
|
||||
(valueAsStringOrNumber, toolCategory) => {
|
||||
const value = Number(valueAsStringOrNumber);
|
||||
|
||||
_getToolNamesFromCategory(toolCategory).forEach(toolName => {
|
||||
updateBrushSize(toolName, value);
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: toolCategory,
|
||||
config: { brushSize: value },
|
||||
},
|
||||
});
|
||||
},
|
||||
[toolGroupService, dispatch]
|
||||
);
|
||||
|
||||
const handleRangeChange = useCallback(
|
||||
newRange => {
|
||||
if (
|
||||
newRange[0] === state.ThresholdBrush.thresholdRange?.[0] &&
|
||||
newRange[1] === state.ThresholdBrush.thresholdRange?.[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolNames = _getToolNamesFromCategory('ThresholdBrush');
|
||||
|
||||
toolNames.forEach(toolName => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: newRange,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: 'ThresholdBrush',
|
||||
config: { thresholdRange: newRange },
|
||||
},
|
||||
});
|
||||
},
|
||||
[toolGroupService, dispatch, state.ThresholdBrush.thresholdRange]
|
||||
);
|
||||
|
||||
return (
|
||||
<AdvancedToolbox
|
||||
title="Segmentation Tools"
|
||||
items={[
|
||||
{
|
||||
name: 'Brush',
|
||||
icon: 'icon-tool-brush',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCULAR_BRUSH ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_BRUSH,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'brush-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.Brush.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'Brush'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'brush-mode',
|
||||
value: state.Brush.mode,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCULAR_BRUSH, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.SPHERE_BRUSH, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Eraser',
|
||||
icon: 'icon-tool-eraser',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCULAR_ERASER ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_ERASER,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
type: 'range',
|
||||
id: 'eraser-radius',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.Eraser.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'Eraser'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: state.Eraser.mode,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCULAR_ERASER, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.SPHERE_ERASER, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Shapes',
|
||||
icon: 'icon-tool-shape',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCLE_SHAPE ||
|
||||
state.activeTool === TOOL_TYPES.RECTANGLE_SHAPE ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_SHAPE,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCLE_SHAPE),
|
||||
options: [
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
value: state.Shapes.mode,
|
||||
id: 'shape-mode',
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCLE_SHAPE, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.RECTANGLE_SHAPE, label: 'Rectangle' },
|
||||
{ value: TOOL_TYPES.SPHERE_SHAPE, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Threshold Tool',
|
||||
icon: 'icon-tool-threshold',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH ||
|
||||
state.activeTool === TOOL_TYPES.THRESHOLD_SPHERE_BRUSH,
|
||||
onClick: () => setToolActive(TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'threshold-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.ThresholdBrush.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'ThresholdBrush'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'threshold-mode',
|
||||
value: state.activeTool,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
id: 'segmentation-threshold-range',
|
||||
children: () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-secondary-light h-[1px]"></div>
|
||||
<div className="mt-1 text-[13px] text-white">Threshold</div>
|
||||
<InputDoubleRange
|
||||
values={state.ThresholdBrush.thresholdRange}
|
||||
onChange={handleRangeChange}
|
||||
minValue={-1000} // Todo: these should be configurable
|
||||
maxValue={1000}
|
||||
step={1}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getToolNamesFromCategory(category) {
|
||||
let toolNames = [];
|
||||
switch (category) {
|
||||
case 'Brush':
|
||||
toolNames = ['CircularBrush', 'SphereBrush'];
|
||||
break;
|
||||
case 'Eraser':
|
||||
toolNames = ['CircularEraser', 'SphereEraser'];
|
||||
break;
|
||||
case 'ThresholdBrush':
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return toolNames;
|
||||
}
|
||||
|
||||
export default SegmentationToolbox;
|
||||
@ -128,13 +128,9 @@ const commandsModule = props => {
|
||||
const definitions = {
|
||||
downloadReport: {
|
||||
commandFn: actions.downloadReport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
storeMeasurements: {
|
||||
commandFn: actions.storeMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.66.7",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.8.0-beta.63",
|
||||
"@ohif/ui": "3.8.0-beta.63",
|
||||
"dcmjs": "^0.29.12",
|
||||
|
||||
@ -109,6 +109,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
initialImageIndex,
|
||||
onReady,
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
@ -184,6 +185,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
if (onElementEnabled) {
|
||||
onElementEnabled(evt);
|
||||
onReady?.(evt);
|
||||
}
|
||||
},
|
||||
[viewportId, onElementEnabled, toolGroupService]
|
||||
|
||||
@ -2,21 +2,25 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { InstanceMetadata } from 'platform/core/src/types';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { InstanceMetadata } from '@ohif/core/src/types';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
import './CustomizableViewportOverlay.css';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
type ViewportData = StackViewportData | VolumeViewportData;
|
||||
|
||||
interface OverlayItemProps {
|
||||
element: any;
|
||||
viewportData: any;
|
||||
element: HTMLElement;
|
||||
viewportData: ViewportData;
|
||||
imageSliceData: ImageSliceData;
|
||||
servicesManager: ServicesManager;
|
||||
viewportId: string;
|
||||
instance: InstanceMetadata;
|
||||
customization: any;
|
||||
formatters: {
|
||||
@ -35,67 +39,11 @@ interface OverlayItemProps {
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Level / Center Overlay item
|
||||
*/
|
||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1 shrink-0">L:</span>
|
||||
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom Level Overlay item
|
||||
*/
|
||||
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">Zoom:</span>
|
||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance Number Overlay Item
|
||||
*/
|
||||
function InstanceNumberOverlayItem({
|
||||
instanceNumber,
|
||||
imageSliceData,
|
||||
customization,
|
||||
}: OverlayItemProps) {
|
||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">I:</span>
|
||||
<span className="font-light">
|
||||
{instanceNumber !== undefined && instanceNumber !== null
|
||||
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const OverlayItemComponents = {
|
||||
'ohif.overlayItem.windowLevel': VOIOverlayItem,
|
||||
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
|
||||
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||
};
|
||||
|
||||
/**
|
||||
* Customizable Viewport Overlay
|
||||
@ -106,12 +54,17 @@ function CustomizableViewportOverlay({
|
||||
imageSliceData,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
viewportData: ViewportData;
|
||||
imageSliceData: ImageSliceData;
|
||||
viewportId: string;
|
||||
servicesManager: ServicesManager;
|
||||
}) {
|
||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
||||
const { cornerstoneViewportService, customizationService, toolGroupService } =
|
||||
servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
const { imageIndex } = imageSliceData;
|
||||
|
||||
const topLeftCustomization = customizationService.getModeCustomization(
|
||||
@ -127,27 +80,18 @@ function CustomizableViewportOverlay({
|
||||
'cornerstoneOverlayBottomRight'
|
||||
);
|
||||
|
||||
const instance = useMemo(() => {
|
||||
if (viewportData != null) {
|
||||
return _getViewportInstance(viewportData, imageIndex);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [viewportData, imageIndex]);
|
||||
const instance = useMemo(
|
||||
() => (viewportData ? getViewportInstance(viewportData, imageIndex) : null),
|
||||
[viewportData, imageIndex]
|
||||
);
|
||||
|
||||
const instanceNumber = useMemo(() => {
|
||||
if (viewportData != null) {
|
||||
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
|
||||
}
|
||||
return null;
|
||||
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
|
||||
|
||||
/**
|
||||
* Initial toolbar state
|
||||
*/
|
||||
useEffect(() => {
|
||||
setActiveTools(toolbarService.getActiveTools());
|
||||
}, []);
|
||||
const instanceNumber = useMemo(
|
||||
() =>
|
||||
viewportData
|
||||
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
|
||||
: null,
|
||||
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
|
||||
);
|
||||
|
||||
/**
|
||||
* Updating the VOI when the viewport changes its voi
|
||||
@ -215,26 +159,9 @@ function CustomizableViewportOverlay({
|
||||
};
|
||||
}, [viewportId, viewportData, cornerstoneViewportService, element]);
|
||||
|
||||
/**
|
||||
* Updating the active tools when the toolbar changes
|
||||
*/
|
||||
// Todo: this should act on the toolGroups instead of the toolbar state
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
setActiveTools(toolbarService.getActiveTools());
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
const _renderOverlayItem = useCallback(
|
||||
item => {
|
||||
const overlayItemProps: OverlayItemProps = {
|
||||
const overlayItemProps = {
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
@ -242,24 +169,26 @@ function CustomizableViewportOverlay({
|
||||
servicesManager,
|
||||
customization: item,
|
||||
formatters: {
|
||||
formatPN: formatPN,
|
||||
formatPN,
|
||||
formatDate: formatDICOMDate,
|
||||
formatTime: formatDICOMTime,
|
||||
formatNumberPrecision: formatNumberPrecision,
|
||||
formatNumberPrecision,
|
||||
},
|
||||
instance,
|
||||
// calculated
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
};
|
||||
|
||||
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
|
||||
return <VOIOverlayItem {...overlayItemProps} />;
|
||||
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
|
||||
return <ZoomOverlayItem {...overlayItemProps} />;
|
||||
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
||||
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { customizationType } = item;
|
||||
const OverlayItemComponent = OverlayItemComponents[customizationType];
|
||||
|
||||
if (OverlayItemComponent) {
|
||||
return <OverlayItemComponent {...overlayItemProps} />;
|
||||
} else {
|
||||
const renderItem = customizationService.transform(item);
|
||||
|
||||
@ -282,103 +211,106 @@ function CustomizableViewportOverlay({
|
||||
]
|
||||
);
|
||||
|
||||
const getTopLeftContent = useCallback(() => {
|
||||
const items = topLeftCustomization?.items || [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [topLeftCustomization, _renderOverlayItem]);
|
||||
const getContent = useCallback(
|
||||
(customization, defaultItems, keyPrefix) => {
|
||||
const items = customization?.items ?? defaultItems;
|
||||
|
||||
const getTopRightContent = useCallback(() => {
|
||||
const items = topRightCustomization?.items || [
|
||||
{
|
||||
id: 'InstanceNmber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [topRightCustomization, _renderOverlayItem]);
|
||||
|
||||
const getBottomLeftContent = useCallback(() => {
|
||||
const items = bottomLeftCustomization?.items || [];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [bottomLeftCustomization, _renderOverlayItem]);
|
||||
|
||||
const getBottomRightContent = useCallback(() => {
|
||||
const items = bottomRightCustomization?.items || [];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [bottomRightCustomization, _renderOverlayItem]);
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
<div key={`${keyPrefix}_${index}`}>
|
||||
{item?.condition
|
||||
? item.condition()
|
||||
? _renderOverlayItem(item)
|
||||
: null
|
||||
: _renderOverlayItem(item)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[_renderOverlayItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
topLeft={getTopLeftContent()}
|
||||
topRight={getTopRightContent()}
|
||||
bottomLeft={getBottomLeftContent()}
|
||||
bottomRight={getBottomRightContent()}
|
||||
topLeft={
|
||||
/**
|
||||
* Inline default overlay items for a more standard expansion
|
||||
*/
|
||||
getContent(
|
||||
topLeftCustomization,
|
||||
[
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||
condition: () => {
|
||||
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
],
|
||||
'topLeftOverlayItem'
|
||||
)
|
||||
}
|
||||
topRight={getContent(
|
||||
topRightCustomization,
|
||||
[
|
||||
{
|
||||
id: 'InstanceNumber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
],
|
||||
'topRightOverlayItem'
|
||||
)}
|
||||
bottomLeft={getContent(bottomLeftCustomization, [null], 'bottomLeftOverlayItem')}
|
||||
bottomRight={getContent(bottomRightCustomization, [null], 'bottomRightOverlayItem')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getViewportInstance(viewportData, imageIndex) {
|
||||
const getViewportInstance = (viewportData, imageIndex) => {
|
||||
const { viewportType, data } = viewportData;
|
||||
let imageId = null;
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
imageId = viewportData.data.imageIds[imageIndex];
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const volumes = viewportData.data;
|
||||
if (volumes && volumes.length == 1) {
|
||||
const volume = volumes[0];
|
||||
imageId = volume.imageIds[imageIndex];
|
||||
}
|
||||
}
|
||||
return imageId ? metaData.get('instance', imageId) || {} : {};
|
||||
}
|
||||
|
||||
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
|
||||
switch (viewportType) {
|
||||
case Enums.ViewportType.STACK:
|
||||
imageId = data.imageIds[imageIndex];
|
||||
break;
|
||||
case Enums.ViewportType.ORTHOGRAPHIC:
|
||||
if (data?.length === 1) {
|
||||
imageId = data[0].imageIds[imageIndex];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return imageId ? metaData.get('instance', imageId) || {} : {};
|
||||
};
|
||||
|
||||
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
|
||||
let instanceNumber;
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||
|
||||
if (!instanceNumber && instanceNumber !== 0) {
|
||||
return null;
|
||||
}
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
switch (viewportData.viewportType) {
|
||||
case Enums.ViewportType.STACK:
|
||||
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||
break;
|
||||
case Enums.ViewportType.ORTHOGRAPHIC:
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
break;
|
||||
}
|
||||
return instanceNumber;
|
||||
}
|
||||
|
||||
return instanceNumber ?? null;
|
||||
};
|
||||
|
||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
const imageIds = viewportData.data.imageIds;
|
||||
@ -442,6 +374,68 @@ function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Level / Center Overlay item
|
||||
*/
|
||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1 shrink-0">L:</span>
|
||||
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom Level Overlay item
|
||||
*/
|
||||
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">Zoom:</span>
|
||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance Number Overlay Item
|
||||
*/
|
||||
function InstanceNumberOverlayItem({
|
||||
instanceNumber,
|
||||
imageSliceData,
|
||||
customization,
|
||||
}: OverlayItemProps) {
|
||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">I:</span>
|
||||
<span className="font-light">
|
||||
{instanceNumber !== undefined && instanceNumber !== null
|
||||
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CustomizableViewportOverlay.propTypes = {
|
||||
viewportData: PropTypes.object,
|
||||
imageIndex: PropTypes.number,
|
||||
|
||||
@ -1,278 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
function CornerstoneViewportOverlay({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
|
||||
/**
|
||||
* Initial toolbar state
|
||||
*/
|
||||
useEffect(() => {
|
||||
setActiveTools(toolbarService.getActiveTools());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTools(toolbarService.getActiveTools());
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updating the VOI when the viewport changes its voi
|
||||
*/
|
||||
useEffect(() => {
|
||||
const updateVOI = eventDetail => {
|
||||
const { range } = eventDetail.detail;
|
||||
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lower, upper } = range;
|
||||
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
|
||||
|
||||
setVOI({ windowCenter, windowWidth });
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||
};
|
||||
}, [viewportId, viewportData, voi, element]);
|
||||
|
||||
/**
|
||||
* Updating the scale when the viewport changes its zoom
|
||||
*/
|
||||
useEffect(() => {
|
||||
const updateScale = eventDetail => {
|
||||
const { previousCamera, camera } = eventDetail.detail;
|
||||
|
||||
if (
|
||||
previousCamera.parallelScale !== camera.parallelScale ||
|
||||
previousCamera.scale !== camera.scale
|
||||
) {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = viewport.getImageData();
|
||||
|
||||
if (!imageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (camera.scale) {
|
||||
setScale(camera.scale);
|
||||
return;
|
||||
}
|
||||
|
||||
const { spacing } = imageData;
|
||||
// convert parallel scale to scale
|
||||
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||
setScale(scale);
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||
};
|
||||
}, [viewportId, viewportData]);
|
||||
|
||||
const getTopLeftContent = useCallback(() => {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
|
||||
if (activeTools.includes('WindowLevel')) {
|
||||
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">W:</span>
|
||||
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1">L:</span>
|
||||
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTools.includes('Zoom')) {
|
||||
return (
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">Zoom:</span>
|
||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [voi, scale, activeTools]);
|
||||
|
||||
const getTopRightContent = useCallback(() => {
|
||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||
if (!viewportData) {
|
||||
return;
|
||||
}
|
||||
|
||||
let instanceNumber;
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||
|
||||
if (!instanceNumber) {
|
||||
return null;
|
||||
}
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">I:</span>
|
||||
<span className="font-light">
|
||||
{instanceNumber !== undefined
|
||||
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}, [imageSliceData, viewportData, viewportId]);
|
||||
|
||||
if (!viewportData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (!ohifViewport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backgroundColor = ohifViewport.getViewportOptions().background;
|
||||
|
||||
// Todo: probably this can be done in a better way in which we identify bright
|
||||
// background
|
||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
topLeft={getTopLeftContent()}
|
||||
topRight={getTopRightContent()}
|
||||
color={isLight && 'text-[#0944B3]'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
const imageIds = viewportData.data.imageIds;
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
|
||||
const { instanceNumber } = generalImageModule;
|
||||
|
||||
const stackSize = imageIds.length;
|
||||
|
||||
if (stackSize <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
|
||||
// Since volume viewports can be in any view direction, they can render
|
||||
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||
function _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
const volumes = viewportData.volumes;
|
||||
|
||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||
if (!volumes || volumes.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = volumes[0];
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!cornerstoneViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const camera = cornerstoneViewport.getCamera();
|
||||
const { viewPlaneNormal } = camera;
|
||||
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
|
||||
|
||||
const scanAxisNormal = direction.slice(6, 9);
|
||||
|
||||
// check if viewPlaneNormal is parallel to scanAxisNormal
|
||||
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
|
||||
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
||||
|
||||
if (isAcquisitionPlane) {
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
}
|
||||
|
||||
CornerstoneViewportOverlay.propTypes = {
|
||||
viewportData: PropTypes.object,
|
||||
imageIndex: PropTypes.number,
|
||||
viewportId: PropTypes.string,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
};
|
||||
|
||||
export default CornerstoneViewportOverlay;
|
||||
@ -21,6 +21,12 @@ import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import { CornerstoneServices } from './types';
|
||||
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||
|
||||
const toggleSyncFunctions = {
|
||||
imageSlice: toggleImageSliceSync,
|
||||
voi: toggleVOISliceSync,
|
||||
};
|
||||
|
||||
function commandsModule({
|
||||
servicesManager,
|
||||
@ -30,11 +36,11 @@ function commandsModule({
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
cineService,
|
||||
toolbarService,
|
||||
uiDialogService,
|
||||
cornerstoneViewportService,
|
||||
uiNotificationService,
|
||||
measurementService,
|
||||
syncGroupService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
const { measurementServiceSource } = this;
|
||||
@ -227,30 +233,10 @@ function commandsModule({
|
||||
arrowTextCallback: ({ callback, data }) => {
|
||||
callInputDialog(uiDialogService, data, callback);
|
||||
},
|
||||
cleanUpCrosshairs: () => {
|
||||
// if the crosshairs tool is active, deactivate it and set window level active
|
||||
// since we are going back to main non-mpr HP
|
||||
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
||||
|
||||
if (activeViewportToolGroup._toolInstances?.Crosshairs?.mode === Enums.ToolModes.Active) {
|
||||
actions.toolbarServiceRecordInteraction({
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleCine: () => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const { isCineEnabled } = cineService.getState();
|
||||
cineService.setIsCineEnabled(!isCineEnabled);
|
||||
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
|
||||
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
||||
},
|
||||
setWindowLevel({ window, level, toolGroupId }) {
|
||||
@ -279,35 +265,40 @@ function commandsModule({
|
||||
});
|
||||
viewport.render();
|
||||
},
|
||||
setToolEnabled: ({ toolName, toggle }) => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// Just call the toolbar service record interaction - allows
|
||||
// executing a toolbar command as a full toolbar command with side affects
|
||||
// coming from the ToolbarService itself.
|
||||
toolbarServiceRecordInteraction: props => {
|
||||
toolbarService.recordInteraction(props);
|
||||
},
|
||||
// Enable or disable a toggleable command, without calling the activation
|
||||
// Used to setup already active tools from hanging protocols
|
||||
setToolbarToggled: props => {
|
||||
toolbarService.setToggled(props.toolId, props.isActive ?? true);
|
||||
},
|
||||
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
|
||||
if (toolName === 'Crosshairs') {
|
||||
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
||||
|
||||
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
|
||||
uiNotificationService.show({
|
||||
title: 'Crosshairs',
|
||||
message:
|
||||
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
throw new Error('Crosshairs tool is not available in this viewport');
|
||||
}
|
||||
if (!viewports.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroup(null);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
|
||||
// Toggle the tool's state only if the toggle is true
|
||||
if (toggle) {
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
}
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
const toolName = itemId || value;
|
||||
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
actions.setToolActive({ toolName, toolGroupId });
|
||||
});
|
||||
},
|
||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
if (!viewports.size) {
|
||||
@ -320,34 +311,13 @@ function commandsModule({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toolGroup.getToolInstance(toolName)) {
|
||||
uiNotificationService.show({
|
||||
title: `${toolName} tool`,
|
||||
message: `The ${toolName} tool is not available in this viewport.`,
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
throw new Error(`ToolGroup ${toolGroup.id} does not have this tool.`);
|
||||
}
|
||||
|
||||
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
||||
|
||||
if (activeToolName) {
|
||||
// Todo: this is a hack to prevent the crosshairs to stick around
|
||||
// after another tool is selected. We should find a better way to do this
|
||||
if (activeToolName === 'Crosshairs') {
|
||||
toolGroup.setToolDisabled(activeToolName);
|
||||
} else {
|
||||
toolGroup.setToolPassive(activeToolName);
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a toggle state, then simply set the enabled/disabled state without
|
||||
// setting the tool active.
|
||||
if (toggledState != null) {
|
||||
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
|
||||
return;
|
||||
const activeToolOptions = toolGroup.getToolConfiguration(activeToolName);
|
||||
activeToolOptions?.disableOnPassive
|
||||
? toolGroup.setToolDisabled(activeToolName)
|
||||
: toolGroup.setToolPassive(activeToolName);
|
||||
}
|
||||
|
||||
// Set the new toolName to be active
|
||||
@ -565,33 +535,59 @@ function commandsModule({
|
||||
(currentIndex + direction + viewportIds.length) % viewportIds.length;
|
||||
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
|
||||
},
|
||||
/**
|
||||
* If the syncId is given and a synchronizer with that ID already exists, it will
|
||||
* toggle it on/off for the provided viewports. If not, it will attempt to create
|
||||
* a new synchronizer using the given syncId and type for the specified viewports.
|
||||
* If no viewports are provided, you may notice some default behavior.
|
||||
* - 'voi' type, we will aim to synchronize all viewports with the same modality
|
||||
* -'imageSlice' type, we will aim to synchronize all viewports with the same orientation.
|
||||
*
|
||||
* @param options
|
||||
* @param options.viewports - The viewports to synchronize
|
||||
* @param options.syncId - The synchronization group ID
|
||||
* @param options.type - The type of synchronization to perform
|
||||
*/
|
||||
toggleSynchronizer: ({ type, viewports, syncId }) => {
|
||||
const synchronizer = syncGroupService.getSynchronizer(syncId);
|
||||
|
||||
toggleImageSliceSync: ({ toggledState }) => {
|
||||
toggleImageSliceSync({
|
||||
servicesManager,
|
||||
toggledState,
|
||||
});
|
||||
if (synchronizer) {
|
||||
synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = toggleSyncFunctions[type];
|
||||
|
||||
if (fn) {
|
||||
fn({
|
||||
servicesManager,
|
||||
viewports,
|
||||
syncId,
|
||||
});
|
||||
}
|
||||
},
|
||||
setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
|
||||
setSourceViewportForReferenceLinesTool: ({ viewportId }) => {
|
||||
if (!viewportId) {
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
viewportId = activeViewportId;
|
||||
viewportId = activeViewportId ?? 'default';
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
toolGroup.setToolConfiguration(
|
||||
toolGroup?.setToolConfiguration(
|
||||
ReferenceLinesTool.toolName,
|
||||
{
|
||||
sourceViewportId: viewportId,
|
||||
},
|
||||
true // overwrite
|
||||
);
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
storePresentation: ({ viewportId }) => {
|
||||
cornerstoneViewportService.storePresentation({ viewportId });
|
||||
},
|
||||
|
||||
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
|
||||
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
const command = protocol.callbacks.onViewportDataInitialized;
|
||||
@ -608,6 +604,26 @@ function commandsModule({
|
||||
}
|
||||
});
|
||||
},
|
||||
resetCrosshairs: ({ viewportId }) => {
|
||||
const crosshairInstances = [];
|
||||
|
||||
const getCrosshairInstances = toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs'));
|
||||
};
|
||||
|
||||
if (!viewportId) {
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(getCrosshairInstances);
|
||||
} else {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
getCrosshairInstances(toolGroup.id);
|
||||
}
|
||||
|
||||
crosshairInstances.forEach(ins => {
|
||||
ins.resetCrosshairs();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -615,7 +631,6 @@ function commandsModule({
|
||||
// context menu
|
||||
showCornerstoneContextMenu: {
|
||||
commandFn: actions.showCornerstoneContextMenu,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
menuCustomizationId: 'measurementsContextMenu',
|
||||
commands: [
|
||||
@ -631,8 +646,6 @@ function commandsModule({
|
||||
},
|
||||
getNearbyAnnotation: {
|
||||
commandFn: actions.getNearbyAnnotation,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
deleteMeasurement: {
|
||||
commandFn: actions.deleteMeasurement,
|
||||
@ -643,16 +656,18 @@ function commandsModule({
|
||||
updateMeasurement: {
|
||||
commandFn: actions.updateMeasurement,
|
||||
},
|
||||
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
},
|
||||
toolbarServiceRecordInteraction: {
|
||||
commandFn: actions.toolbarServiceRecordInteraction,
|
||||
},
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
},
|
||||
setToolActiveToolbar: {
|
||||
commandFn: actions.setToolActiveToolbar,
|
||||
},
|
||||
setToolEnabled: {
|
||||
commandFn: actions.setToolEnabled,
|
||||
},
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
options: { rotation: 90 },
|
||||
@ -735,15 +750,15 @@ function commandsModule({
|
||||
storePresentation: {
|
||||
commandFn: actions.storePresentation,
|
||||
},
|
||||
setToolbarToggled: {
|
||||
commandFn: actions.setToolbarToggled,
|
||||
},
|
||||
cleanUpCrosshairs: {
|
||||
commandFn: actions.cleanUpCrosshairs,
|
||||
},
|
||||
attachProtocolViewportDataListener: {
|
||||
commandFn: actions.attachProtocolViewportDataListener,
|
||||
},
|
||||
resetCrosshairs: {
|
||||
commandFn: actions.resetCrosshairs,
|
||||
},
|
||||
toggleSynchronizer: {
|
||||
commandFn: actions.toggleSynchronizer,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,38 +1,18 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { CinePlayer, useCine } from '@ohif/ui';
|
||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
const {
|
||||
toolbarService,
|
||||
customizationService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cineService,
|
||||
} = servicesManager.services;
|
||||
const [{ isCineEnabled, cines }] = useCine();
|
||||
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
||||
const [{ isCineEnabled, cines }, api] = useCine();
|
||||
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
|
||||
const [appConfig] = useAppConfig();
|
||||
const isMountedRef = useRef(null);
|
||||
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
|
||||
const handleCineClose = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MoreTools',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
commandOptions: {},
|
||||
toolName: 'cine',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
||||
return;
|
||||
@ -45,11 +25,11 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(enabledVPElement, {
|
||||
api.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
api.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
|
||||
@ -70,34 +50,36 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
});
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.setIsCineEnabled(isPlaying);
|
||||
api.setIsCineEnabled(isPlaying);
|
||||
}
|
||||
cineService.setCine({ id: viewportId, isPlaying, frameRate });
|
||||
api.setCine({ id: viewportId, isPlaying, frameRate });
|
||||
setNewStackFrameRate(frameRate);
|
||||
}, [cineService, displaySetService, viewportId, viewportGridService, cines]);
|
||||
}, [displaySetService, viewportId, viewportGridService, cines]);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
isMountedRef.current = false;
|
||||
api.stopClip(enabledVPElement);
|
||||
api.setCine({ id: viewportId, isPlaying: false });
|
||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
||||
};
|
||||
}, [enabledVPElement, newStackCineHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
api.stopClip(enabledVPElement);
|
||||
};
|
||||
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
||||
}, [cines, viewportId, enabledVPElement, cineHandler]);
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
@ -108,15 +90,22 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||
frameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
onClose={() => {
|
||||
// also stop the clip
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
isPlaying: false,
|
||||
});
|
||||
api.setIsCineEnabled(false);
|
||||
}}
|
||||
onPlayPauseChange={isPlaying => {
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
frameRate,
|
||||
})
|
||||
|
||||
251
extensions/cornerstone/src/getToolbarModule.tsx
Normal file
251
extensions/cornerstone/src/getToolbarModule.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
|
||||
const getToggledClassName = (isToggled: boolean) => {
|
||||
return isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
||||
};
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const {
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
return [
|
||||
// functions/helpers to be used by the toolbar buttons to decide if they should
|
||||
// enabled or not
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
};
|
||||
}
|
||||
|
||||
const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName;
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black bg-primary-light'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
|
||||
// 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 = 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: ({ viewportId, button }) => {
|
||||
return {
|
||||
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
};
|
||||
}
|
||||
|
||||
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
|
||||
toolGroup.getToolOptions(toolName).mode
|
||||
);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.synchronizer',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
let synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
|
||||
|
||||
if (!synchronizers?.length) {
|
||||
return {
|
||||
className: getToggledClassName(false),
|
||||
};
|
||||
}
|
||||
|
||||
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
|
||||
|
||||
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
|
||||
|
||||
if (!synchronizers?.length) {
|
||||
return {
|
||||
className: getToggledClassName(false),
|
||||
};
|
||||
}
|
||||
|
||||
// Todo: we need a better way to find the synchronizers based on their
|
||||
// type, but for now we just check the first one and see if it is
|
||||
// enabled
|
||||
const synchronizer = synchronizers[0];
|
||||
|
||||
const isEnabled = synchronizer?._enabled;
|
||||
|
||||
return {
|
||||
className: getToggledClassName(isEnabled),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.viewportProperties.toggle',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport || viewport.isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propId = button.id;
|
||||
|
||||
const properties = viewport.getProperties();
|
||||
const camera = viewport.getCamera();
|
||||
|
||||
const prop = camera?.[propId] || properties?.[propId];
|
||||
|
||||
if (!prop) {
|
||||
return {
|
||||
disabled: false,
|
||||
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
}
|
||||
|
||||
const isToggled = prop;
|
||||
|
||||
return {
|
||||
className: getToggledClassName(isToggled),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.mpr',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
if (!displaySetUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||
|
||||
const areReconstructable = displaySets.every(displaySet => {
|
||||
return displaySet.isReconstructable;
|
||||
});
|
||||
|
||||
if (!areReconstructable) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
};
|
||||
}
|
||||
|
||||
const isMpr = protocol?.id === 'mpr';
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: getToggledClassName(isMpr),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getToolNameForButton(button) {
|
||||
const { props } = button;
|
||||
|
||||
const commands = props?.commands || button.commands;
|
||||
const commandsArray = Array.isArray(commands) ? commands : [commands];
|
||||
const firstCommand = commandsArray[0];
|
||||
if (typeof firstCommand === 'string') {
|
||||
// likely not a cornerstone tool
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('commandOptions' in firstCommand) {
|
||||
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
|
||||
}
|
||||
|
||||
// use id as a fallback for toolName
|
||||
return props?.id ?? button.id;
|
||||
}
|
||||
@ -22,11 +22,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
||||
},
|
||||
],
|
||||
// Turns off crosshairs when switching out of MPR mode
|
||||
onProtocolExit: [
|
||||
{
|
||||
commandName: 'cleanUpCrosshairs',
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySetSelectors: {
|
||||
activeDisplaySet: {
|
||||
|
||||
@ -13,6 +13,7 @@ import init from './init';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getToolbarModule from './getToolbarModule';
|
||||
import ToolGroupService from './services/ToolGroupService';
|
||||
import SyncGroupService from './services/SyncGroupService';
|
||||
import SegmentationService from './services/SegmentationService';
|
||||
@ -26,7 +27,6 @@ import dicomLoaderService from './utils/dicomLoaderService';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
|
||||
import { id } from './id';
|
||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
@ -79,6 +79,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
return init.call(this, props);
|
||||
},
|
||||
|
||||
getToolbarModule,
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
|
||||
@ -15,7 +15,6 @@ import {
|
||||
utilities as csUtilities,
|
||||
Enums as csEnums,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
|
||||
import initWADOImageLoader from './initWADOImageLoader';
|
||||
@ -41,7 +40,6 @@ export default async function init({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
appConfig,
|
||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
// Note: this should run first before initializing the cornerstone
|
||||
@ -94,13 +92,26 @@ export default async function init({
|
||||
cineService,
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
toolGroupService,
|
||||
toolbarService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
syncGroupService,
|
||||
segmentationService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
|
||||
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(eventTarget, [
|
||||
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
|
||||
]);
|
||||
|
||||
window.services = servicesManager.services;
|
||||
window.extensionManager = extensionManager;
|
||||
window.commandsManager = commandsManager;
|
||||
@ -225,47 +236,6 @@ export default async function init({
|
||||
commandsManager,
|
||||
});
|
||||
|
||||
/**
|
||||
* When a viewport gets a new display set, this call will go through all the
|
||||
* active tools in the toolbar, and call any commands registered in the
|
||||
* toolbar service with a callback to re-enable on displaying the viewport.
|
||||
*/
|
||||
const toolbarEventListener = evt => {
|
||||
const { element } = evt.detail;
|
||||
const activeTools = toolbarService.getActiveTools();
|
||||
|
||||
activeTools.forEach(tool => {
|
||||
const toolData = toolbarService.getNestedButton(tool);
|
||||
const commands = toolData?.listeners?.[evt.type];
|
||||
commandsManager.run(commands, { element, evt });
|
||||
});
|
||||
};
|
||||
|
||||
/** Listens for active viewport events and fires the toolbar listeners */
|
||||
const activeViewportEventListener = evt => {
|
||||
const { viewportId } = evt;
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
const activeTools = toolbarService.getActiveTools();
|
||||
|
||||
activeTools.forEach(tool => {
|
||||
if (!toolGroup?._toolInstances?.[tool]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if tool is active on the new viewport
|
||||
const toolEnabled = toolGroup._toolInstances[tool].mode === Enums.ToolModes.Enabled;
|
||||
|
||||
if (!toolEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = toolbarService.getNestedButton(tool);
|
||||
const commands = button?.listeners?.[evt.type];
|
||||
commandsManager.run(commands, { viewportId, evt });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs error handler for failed requests.
|
||||
* @param event
|
||||
@ -275,30 +245,6 @@ export default async function init({
|
||||
handler(detail.error);
|
||||
};
|
||||
|
||||
const resetCrosshairs = evt => {
|
||||
const { element } = evt.detail;
|
||||
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(element);
|
||||
|
||||
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
if (!toolGroup || !toolGroup._toolInstances?.['Crosshairs']) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = toolGroup._toolInstances['Crosshairs'].mode;
|
||||
|
||||
if (mode === Enums.ToolModes.Active) {
|
||||
toolGroup.setToolActive('Crosshairs');
|
||||
} else if (mode === Enums.ToolModes.Passive) {
|
||||
toolGroup.setToolPassive('Crosshairs');
|
||||
} else if (mode === Enums.ToolModes.Enabled) {
|
||||
toolGroup.setToolEnabled('Crosshairs');
|
||||
}
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
||||
const { element } = evt.detail;
|
||||
cornerstoneTools.utilities.stackContextPrefetch.enable(element);
|
||||
@ -309,17 +255,21 @@ export default async function init({
|
||||
function elementEnabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, evt => {
|
||||
const { element } = evt.detail;
|
||||
const { viewportId } = getEnabledElement(element);
|
||||
commandsManager.runCommand('resetCrosshairs', { viewportId });
|
||||
});
|
||||
|
||||
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
|
||||
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
|
||||
|
||||
initViewTiming({ element, eventTarget });
|
||||
initViewTiming({ element });
|
||||
}
|
||||
|
||||
function elementDisabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
// element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
// TODO - consider removing the callback when all elements are gone
|
||||
// eventTarget.removeEventListener(
|
||||
@ -332,10 +282,10 @@ export default async function init({
|
||||
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
activeViewportEventListener
|
||||
);
|
||||
// viewportGridService.subscribe(
|
||||
// viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
// activeViewportEventListener
|
||||
// );
|
||||
}
|
||||
|
||||
function CPUModal() {
|
||||
|
||||
@ -962,7 +962,7 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
// Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
|
||||
// it is easily compressible in worker thread.
|
||||
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
|
||||
await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, {
|
||||
volumeId: segmentationId,
|
||||
targetBuffer: {
|
||||
type: 'Uint8Array',
|
||||
@ -1368,8 +1368,6 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
public setConfiguration = (configuration: SegmentationConfig): void => {
|
||||
const {
|
||||
brushSize,
|
||||
brushThresholdGate,
|
||||
fillAlpha,
|
||||
fillAlphaInactive,
|
||||
outlineWidthActive,
|
||||
@ -1461,7 +1459,6 @@ class SegmentationService extends PubSubService {
|
||||
segmentInfo.label = label;
|
||||
|
||||
if (suppressEvents === false) {
|
||||
// this._setSegmentationModified(segmentationId);
|
||||
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||||
segmentation,
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
|
||||
import { getRenderingEngines } from '@cornerstonejs/core';
|
||||
|
||||
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
|
||||
|
||||
@ -25,6 +26,7 @@ const POSITION = 'cameraposition';
|
||||
const VOI = 'voi';
|
||||
const ZOOMPAN = 'zoompan';
|
||||
const STACKIMAGE = 'stackimage';
|
||||
const IMAGE_SLICE = 'imageslice';
|
||||
|
||||
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
||||
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||
@ -45,9 +47,14 @@ export default class SyncGroupService {
|
||||
[POSITION]: synchronizers.createCameraPositionSynchronizer,
|
||||
[VOI]: synchronizers.createVOISynchronizer,
|
||||
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
|
||||
// todo: remove stack image since it is legacy now and the image_slice
|
||||
// handles both stack and volume viewports
|
||||
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
|
||||
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
|
||||
};
|
||||
|
||||
synchronizersByType: { [key: string]: Synchronizer[] } = {};
|
||||
|
||||
constructor(serviceManager: ServicesManager) {
|
||||
this.servicesManager = serviceManager;
|
||||
this.listeners = {};
|
||||
@ -57,14 +64,27 @@ export default class SyncGroupService {
|
||||
}
|
||||
|
||||
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
|
||||
// Initialize if not already done
|
||||
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
|
||||
|
||||
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
|
||||
|
||||
if (syncCreator) {
|
||||
return syncCreator(id, options);
|
||||
const synchronizer = syncCreator(id, options);
|
||||
|
||||
if (synchronizer) {
|
||||
this.synchronizersByType[type].push(synchronizer);
|
||||
return synchronizer;
|
||||
}
|
||||
} else {
|
||||
console.warn('Unknown synchronizer type', type, id);
|
||||
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getSyncCreatorForType(type: string): SyncCreator {
|
||||
return this.synchronizerCreators[type.toLowerCase()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a synchronizer type.
|
||||
* @param type is the type of the synchronizer to create
|
||||
@ -78,6 +98,15 @@ export default class SyncGroupService {
|
||||
return SynchronizerManager.getSynchronizer(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of synchronizers of a specific type.
|
||||
* @param type - The type of synchronizers to retrieve.
|
||||
* @returns An array of synchronizers of the specified type.
|
||||
*/
|
||||
public getSynchronizersOfType(type: string): Synchronizer[] {
|
||||
return this.synchronizersByType[type];
|
||||
}
|
||||
|
||||
protected _getOrCreateSynchronizer(
|
||||
type: string,
|
||||
id: string,
|
||||
@ -125,14 +154,17 @@ export default class SyncGroupService {
|
||||
SynchronizerManager.destroy();
|
||||
}
|
||||
|
||||
public getSynchronizersForViewport(
|
||||
viewportId: string,
|
||||
renderingEngineId: string
|
||||
): Synchronizer[] {
|
||||
return SynchronizerManager.getAllSynchronizers().filter(
|
||||
public getSynchronizersForViewport(viewportId: string): Synchronizer[] {
|
||||
const renderingEngine =
|
||||
getRenderingEngines().find(re => {
|
||||
return re.getViewports().find(vp => vp.id === viewportId);
|
||||
}) || getRenderingEngines()[0];
|
||||
|
||||
const synchronizers = SynchronizerManager.getAllSynchronizers();
|
||||
return synchronizers.filter(
|
||||
s =>
|
||||
s.hasSourceViewport(renderingEngineId, viewportId) ||
|
||||
s.hasTargetViewport(renderingEngineId, viewportId)
|
||||
s.hasSourceViewport(renderingEngine.id, viewportId) ||
|
||||
s.hasTargetViewport(renderingEngine.id, viewportId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -189,19 +189,6 @@ export default class ToolGroupService {
|
||||
this.addToolsToToolGroup(toolGroupId, tools);
|
||||
return toolGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
private changeConfigurationIfNecessary(toolGroup, volumeUID) {
|
||||
// handle specific assignment for volumeUID (e.g., fusion)
|
||||
const toolInstances = toolGroup._toolInstances;
|
||||
// Object.values(toolInstances).forEach(toolInstance => {
|
||||
// if (toolInstance.configuration) {
|
||||
// toolInstance.configuration.volumeUID = volumeUID;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the tool's configuration based on the tool name and tool group id
|
||||
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
|
||||
|
||||
@ -234,7 +234,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
const { viewPlaneNormal, viewUp } = csViewport.getCamera();
|
||||
const initialImageIndex = csViewport.getCurrentImageIdIndex();
|
||||
const initialImageIndex = csViewport.getCurrentImageIdIndex() || 0;
|
||||
const zoom = csViewport.getZoom();
|
||||
const pan = csViewport.getPan();
|
||||
|
||||
@ -347,10 +347,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
const { stateSyncService, syncGroupService } = this.servicesManager.services;
|
||||
|
||||
const synchronizers = syncGroupService.getSynchronizersForViewport(
|
||||
viewportId,
|
||||
this.renderingEngine.id
|
||||
);
|
||||
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
|
||||
|
||||
const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
|
||||
stateSyncService.getState();
|
||||
|
||||
@ -189,7 +189,7 @@ const CornerstoneViewportDownloadForm = ({
|
||||
// add the viewport to the toolGroup
|
||||
toolGroup.addViewport(downloadViewportId, renderingEngineId);
|
||||
|
||||
Object.keys(toolGroup._toolInstances).forEach(toolName => {
|
||||
Object.keys(toolGroup.getToolInstances()).forEach(toolName => {
|
||||
// make all tools Enabled so that they can not be interacted with
|
||||
// in the download viewport
|
||||
if (toggle && toolName !== 'Crosshairs') {
|
||||
|
||||
@ -1,20 +1,35 @@
|
||||
const IMAGE_SLICE_SYNC_NAME = 'IMAGE_SLICE_SYNC';
|
||||
|
||||
export default function toggleImageSliceSync({
|
||||
toggledState,
|
||||
servicesManager,
|
||||
viewports: providedViewports,
|
||||
syncId,
|
||||
}) {
|
||||
if (!toggledState) {
|
||||
return disableSync(IMAGE_SLICE_SYNC_NAME, servicesManager);
|
||||
}
|
||||
|
||||
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
|
||||
servicesManager.services;
|
||||
|
||||
syncId ||= IMAGE_SLICE_SYNC_NAME;
|
||||
|
||||
const viewports =
|
||||
providedViewports || getReconstructableStackViewports(viewportGridService, displaySetService);
|
||||
|
||||
// Todo: right now we don't have a proper way to define specific
|
||||
// viewports to add to synchronizers, and right now it is global or not
|
||||
// after we do that, we should do fine grained control of the synchronizers
|
||||
const someViewportHasSync = viewports.some(viewport => {
|
||||
const syncStates = syncGroupService.getSynchronizersForViewport(
|
||||
viewport.viewportOptions.viewportId
|
||||
);
|
||||
|
||||
const imageSync = syncStates.find(syncState => syncState.id === syncId);
|
||||
|
||||
return !!imageSync;
|
||||
});
|
||||
|
||||
if (someViewportHasSync) {
|
||||
return disableSync(syncId, servicesManager);
|
||||
}
|
||||
|
||||
// create synchronization group and add the viewports to it.
|
||||
viewports.forEach(gridViewport => {
|
||||
const { viewportId } = gridViewport.viewportOptions;
|
||||
@ -23,8 +38,8 @@ export default function toggleImageSliceSync({
|
||||
return;
|
||||
}
|
||||
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
|
||||
type: 'stackimage',
|
||||
id: IMAGE_SLICE_SYNC_NAME,
|
||||
type: 'imageSlice',
|
||||
id: syncId,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
|
||||
94
extensions/cornerstone/src/utils/toggleVOISliceSync.ts
Normal file
94
extensions/cornerstone/src/utils/toggleVOISliceSync.ts
Normal file
@ -0,0 +1,94 @@
|
||||
const VOI_SYNC_NAME = 'VOI_SYNC';
|
||||
|
||||
const getSyncId = modality => `${VOI_SYNC_NAME}_${modality}`;
|
||||
|
||||
export default function toggleVOISliceSync({
|
||||
servicesManager,
|
||||
viewports: providedViewports,
|
||||
syncId,
|
||||
}) {
|
||||
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
|
||||
servicesManager.services;
|
||||
|
||||
const viewports =
|
||||
providedViewports || groupViewportsByModality(viewportGridService, displaySetService);
|
||||
|
||||
// Todo: right now we don't have a proper way to define specific
|
||||
// viewports to add to synchronizers, and right now it is global or not
|
||||
// after we do that, we should do fine grained control of the synchronizers
|
||||
|
||||
// we can apply voi sync within each modality group
|
||||
for (const [modality, modalityViewports] of Object.entries(viewports)) {
|
||||
const syncIdToUse = syncId || getSyncId(modality);
|
||||
|
||||
const someViewportHasSync = modalityViewports.some(viewport => {
|
||||
const syncStates = syncGroupService.getSynchronizersForViewport(
|
||||
viewport.viewportOptions.viewportId
|
||||
);
|
||||
|
||||
const imageSync = syncStates.find(syncState => syncState.id === syncIdToUse);
|
||||
|
||||
return !!imageSync;
|
||||
});
|
||||
|
||||
if (someViewportHasSync) {
|
||||
return disableSync(modalityViewports, syncIdToUse, servicesManager);
|
||||
}
|
||||
|
||||
// create synchronization group and add the modalityViewports to it.
|
||||
modalityViewports.forEach(gridViewport => {
|
||||
const { viewportId } = gridViewport.viewportOptions;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
|
||||
type: 'voi',
|
||||
id: syncIdToUse,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function disableSync(modalityViewports, syncId, servicesManager) {
|
||||
const { syncGroupService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const viewports = modalityViewports;
|
||||
viewports.forEach(gridViewport => {
|
||||
const { viewportId } = gridViewport.viewportOptions;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
syncGroupService.removeViewportFromSyncGroup(
|
||||
viewport.id,
|
||||
viewport.getRenderingEngine().id,
|
||||
syncId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function groupViewportsByModality(viewportGridService, displaySetService) {
|
||||
let { viewports } = viewportGridService.getState();
|
||||
|
||||
viewports = [...viewports.values()];
|
||||
|
||||
// group the viewports by modality
|
||||
return viewports.reduce((acc, viewport) => {
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
// Todo: add proper fusion support
|
||||
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
const modality = displaySet.Modality;
|
||||
if (!acc[modality]) {
|
||||
acc[modality] = [];
|
||||
}
|
||||
|
||||
acc[modality].push(viewport);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
@ -60,7 +60,6 @@ export default class ContextMenuController {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Getting items from', menus);
|
||||
const items = ContextMenuItemsBuilder.getMenuItems(
|
||||
selectorProps || contextMenuProps,
|
||||
event,
|
||||
|
||||
@ -70,8 +70,6 @@ export function findMenu(menus: Menu[], props?: Types.IProps, menuIdFilter?: str
|
||||
current = findIt.next();
|
||||
}
|
||||
|
||||
console.log('Menu chosen', menu?.id || 'NONE');
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,11 @@
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
// Note: the default width and height of the canvas is 300x150
|
||||
// but we need to set the width and height to the same number since
|
||||
// the thumbnails are usually square and we want to maintain the aspect ratio
|
||||
canvas.width = 128 / window.devicePixelRatio;
|
||||
canvas.height = 128 / window.devicePixelRatio;
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas({ canvas, imageId })
|
||||
.then(imageId => {
|
||||
|
||||
@ -1,57 +1,53 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
|
||||
export default function Toolbar({
|
||||
servicesManager,
|
||||
}: Types.Extensions.ExtensionParams): React.ReactElement {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
export function Toolbar({ servicesManager }) {
|
||||
const { toolbarButtons, onInteraction } = useToolbar({
|
||||
servicesManager,
|
||||
buttonSection: 'primary',
|
||||
});
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateToolbar = () => {
|
||||
const toolGroupId =
|
||||
viewportGridService.getActiveViewportOptionByKey('toolGroupId') ?? 'default';
|
||||
setToolbarButtons(toolbarService.getButtonSection(toolGroupId));
|
||||
};
|
||||
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||
updateToolbar
|
||||
);
|
||||
|
||||
updateToolbar();
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService, viewportGrid]);
|
||||
|
||||
const onInteraction = useCallback(
|
||||
args => toolbarService.recordInteraction(args),
|
||||
[toolbarService]
|
||||
);
|
||||
if (!toolbarButtons.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolbarButtons.map(toolDef => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
return (
|
||||
// The margin for separating the tools on the toolbar should go here and NOT in each individual component (button) item.
|
||||
// This allows for the individual items to be included in other UI components where perhaps alternative margins are desired.
|
||||
const { disabled } = componentProps;
|
||||
|
||||
const tool = (
|
||||
<Component
|
||||
key={id}
|
||||
id={id}
|
||||
onInteraction={onInteraction}
|
||||
servicesManager={servicesManager}
|
||||
{...componentProps}
|
||||
/>
|
||||
);
|
||||
|
||||
return disabled ? (
|
||||
<Tooltip
|
||||
key={id}
|
||||
position="bottom"
|
||||
content={componentProps.label}
|
||||
secondaryContent={'Not available on the current viewport'}
|
||||
>
|
||||
<div className={classnames('mr-1')}>{tool}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div
|
||||
key={id}
|
||||
className={classnames('mr-1')}
|
||||
className="mr-1"
|
||||
>
|
||||
<Component
|
||||
id={id}
|
||||
{...componentProps}
|
||||
onInteraction={onInteraction}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{tool}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) {
|
||||
const getSplitButtonItems = useCallback(
|
||||
items =>
|
||||
items.map((item, index) => (
|
||||
<ToolbarButton
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
disabled={item.disabled}
|
||||
className={item.className}
|
||||
id={item.id}
|
||||
size={size}
|
||||
onClick={() => {
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: item.id,
|
||||
commands: item.commands,
|
||||
});
|
||||
}}
|
||||
// Note: this is necessary since tooltip will add
|
||||
// default styles to the tooltip container which
|
||||
// we don't want for groups
|
||||
toolTipClassName=""
|
||||
/>
|
||||
)),
|
||||
[onInteraction, groupId]
|
||||
);
|
||||
|
||||
return <ButtonGroup>{getSplitButtonItems(items)}</ButtonGroup>;
|
||||
}
|
||||
|
||||
export default ToolbarButtonGroupWithServices;
|
||||
@ -1,75 +0,0 @@
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ToolbarButtonWithServices({
|
||||
id,
|
||||
type,
|
||||
commands,
|
||||
onInteraction,
|
||||
servicesManager,
|
||||
...props
|
||||
}) {
|
||||
const { toolbarService } = servicesManager?.services || {};
|
||||
|
||||
const [buttonsState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
const { primaryToolId } = buttonsState;
|
||||
|
||||
const isActive =
|
||||
(type === 'tool' && id === primaryToolId) ||
|
||||
(type === 'toggle' && buttonsState.toggles[id] === true);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
state => {
|
||||
setButtonState({ ...state });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
commands={commands}
|
||||
id={id}
|
||||
type={type}
|
||||
isActive={isActive}
|
||||
onInteraction={onInteraction}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarButtonWithServices.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
commands: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string.isRequired,
|
||||
context: PropTypes.string,
|
||||
})
|
||||
),
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
toolbarService: PropTypes.shape({
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
state: PropTypes.shape({
|
||||
primaryToolId: PropTypes.string,
|
||||
toggles: PropTypes.objectOf(PropTypes.bool),
|
||||
groups: PropTypes.objectOf(PropTypes.any),
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ToolbarButtonWithServices;
|
||||
@ -3,21 +3,20 @@ import PropTypes from 'prop-types';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
function ToolbarLayoutSelectorWithServices({ servicesManager, ...props }) {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
function ToolbarLayoutSelectorWithServices({ servicesManager, commands, ...props }) {
|
||||
const { toolbarService, viewportGridService } = servicesManager.services;
|
||||
|
||||
const onSelection = useCallback(
|
||||
props => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
commandOptions: { ...props },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
});
|
||||
const onInteraction = useCallback(
|
||||
args => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const refreshProps = {
|
||||
viewportId,
|
||||
};
|
||||
|
||||
if (props.onLayoutChange) {
|
||||
props.onLayoutChange(args);
|
||||
}
|
||||
toolbarService.recordInteraction({ commands }, { ...args, refreshProps });
|
||||
},
|
||||
[toolbarService]
|
||||
);
|
||||
@ -25,12 +24,12 @@ function ToolbarLayoutSelectorWithServices({ servicesManager, ...props }) {
|
||||
return (
|
||||
<LayoutSelector
|
||||
{...props}
|
||||
onSelection={onSelection}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutSelector({ rows, columns, className, onSelection, ...rest }) {
|
||||
function LayoutSelector({ rows, columns, className, onInteraction, ...rest }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const closeOnOutsideClick = () => {
|
||||
@ -62,7 +61,7 @@ function LayoutSelector({ rows, columns, className, onSelection, ...rest }) {
|
||||
<DropdownContent
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
onSelection={onSelection}
|
||||
onSelection={onInteraction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { SplitButton, Icon, ToolbarButton } from '@ohif/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SplitButton, ToolbarButton } from '@ohif/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
function ToolbarSplitButtonWithServices({
|
||||
isRadio,
|
||||
isAction,
|
||||
groupId,
|
||||
primary,
|
||||
secondary,
|
||||
@ -16,123 +13,35 @@ function ToolbarSplitButtonWithServices({
|
||||
}) {
|
||||
const { toolbarService } = servicesManager?.services;
|
||||
|
||||
const handleItemClick = (item, index) => {
|
||||
const { id, type, commands } = item;
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: id,
|
||||
interactionType: type,
|
||||
commands,
|
||||
});
|
||||
|
||||
setState(state => ({
|
||||
...state,
|
||||
primary: !isAction && isRadio ? { ...item, index } : state.primary,
|
||||
isExpanded: false,
|
||||
items: getSplitButtonItems(items).filter(item =>
|
||||
isRadio && !isAction ? item.index !== index : true
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
/* Bubbles up individual item clicks */
|
||||
const getSplitButtonItems = items =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
index,
|
||||
onClick: () => handleItemClick(item, index),
|
||||
}));
|
||||
|
||||
const [buttonsState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
|
||||
const [state, setState] = useState({
|
||||
primary,
|
||||
items: getSplitButtonItems(items).filter(item =>
|
||||
isRadio && !isAction ? item.id !== primary.id : true
|
||||
),
|
||||
});
|
||||
|
||||
const { primaryToolId, toggles } = buttonsState;
|
||||
|
||||
const isPrimaryToggle = state.primary.type === 'toggle';
|
||||
|
||||
const isPrimaryActive =
|
||||
(state.primary.type === 'tool' && primaryToolId === state.primary.id) ||
|
||||
(isPrimaryToggle && toggles[state.primary.id] === true);
|
||||
const getSplitButtonItems = useCallback(
|
||||
items =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
index,
|
||||
onClick: () => {
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: item.id,
|
||||
commands: item.commands,
|
||||
});
|
||||
},
|
||||
})),
|
||||
[]
|
||||
);
|
||||
|
||||
const PrimaryButtonComponent =
|
||||
toolbarService?.getButtonComponentForUIType(state.primary.uiType) ?? ToolbarButton;
|
||||
toolbarService?.getButtonComponentForUIType(primary.uiType) ?? ToolbarButton;
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
state => {
|
||||
setButtonState({ ...state });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
const updatedItems = state.items.map(item => {
|
||||
const isActive = item.type === 'tool' && primaryToolId === item.id;
|
||||
|
||||
// We could have added the
|
||||
// item.type === 'toggle' && toggles[item.id] === true
|
||||
// too but that makes the button active when the toggle is active under it
|
||||
// which feels weird
|
||||
return {
|
||||
...item,
|
||||
isActive,
|
||||
};
|
||||
});
|
||||
|
||||
const DefaultListItemRenderer = ({ type, icon, label, t, id }) => {
|
||||
const isActive = type === 'toggle' && toggles[id] === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'hover:bg-primary-dark flex h-8 w-full flex-row items-center p-3',
|
||||
'whitespace-pre text-base',
|
||||
isActive && 'bg-primary-dark',
|
||||
isActive
|
||||
? 'text-[#348CFD]'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light'
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<span className="mr-4">
|
||||
<Icon
|
||||
name={icon}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="mr-5">{t(label)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const listItemRenderer = renderer || DefaultListItemRenderer;
|
||||
const listItemRenderer = renderer;
|
||||
|
||||
return (
|
||||
<SplitButton
|
||||
isRadio={isRadio}
|
||||
isAction={isAction}
|
||||
primary={state.primary}
|
||||
primary={primary}
|
||||
secondary={secondary}
|
||||
items={updatedItems}
|
||||
items={getSplitButtonItems(items)}
|
||||
groupId={groupId}
|
||||
renderer={listItemRenderer}
|
||||
isActive={isPrimaryActive || updatedItems.some(item => item.isActive)}
|
||||
isToggle={isPrimaryToggle}
|
||||
onInteraction={onInteraction}
|
||||
Component={props => (
|
||||
<PrimaryButtonComponent
|
||||
@ -146,11 +55,9 @@ function ToolbarSplitButtonWithServices({
|
||||
|
||||
ToolbarSplitButtonWithServices.propTypes = {
|
||||
isRadio: PropTypes.bool,
|
||||
isAction: PropTypes.bool,
|
||||
groupId: PropTypes.string,
|
||||
primary: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
uiType: PropTypes.string,
|
||||
}),
|
||||
secondary: PropTypes.shape({
|
||||
@ -158,15 +65,17 @@ ToolbarSplitButtonWithServices.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
}),
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
icon: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
})
|
||||
),
|
||||
renderer: PropTypes.func,
|
||||
|
||||
@ -7,7 +7,7 @@ import { ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@o
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
import { Toolbar } from '../Toolbar/Toolbar';
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
|
||||
@ -48,22 +48,13 @@ function ViewerLayout({
|
||||
const getComponent = id => {
|
||||
const entry = extensionManager.getModuleEntry(id);
|
||||
|
||||
if (!entry) {
|
||||
if (!entry || !entry.component) {
|
||||
throw new Error(
|
||||
`${id} is not valid for an extension module. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file)`
|
||||
`${id} is not valid for an extension module or no component found from extension ${id}. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file). Check the reference string to the extension in your Mode configuration`
|
||||
);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (entry && entry.component) {
|
||||
content = entry.component;
|
||||
} else {
|
||||
throw new Error(
|
||||
`No component found from extension ${id}. Check the reference string to the extension in your Mode configuration`
|
||||
);
|
||||
}
|
||||
|
||||
return { entry, content };
|
||||
return { entry, content: entry.component };
|
||||
};
|
||||
|
||||
const getPanelData = id => {
|
||||
@ -76,6 +67,7 @@ function ViewerLayout({
|
||||
label: entry.label,
|
||||
name: entry.name,
|
||||
content,
|
||||
contexts: entry.contexts,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ export type HangingProtocolParams = {
|
||||
stageIndex?: number;
|
||||
activeStudyUID?: string;
|
||||
stageId?: string;
|
||||
reset?: false;
|
||||
};
|
||||
|
||||
export type UpdateViewportDisplaySetParams = {
|
||||
@ -107,41 +108,6 @@ const commandsModule = ({
|
||||
measurementService.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles off all tools which contain a commandName of setHangingProtocol
|
||||
* or toggleHangingProtocol, and which match/don't match the protocol id/stage
|
||||
*/
|
||||
toggleHpTools: () => {
|
||||
const {
|
||||
protocol,
|
||||
stageIndex: toggleStageIndex,
|
||||
stage,
|
||||
} = hangingProtocolService.getActiveProtocol();
|
||||
const enableListener = button => {
|
||||
if (!button.id) {
|
||||
return;
|
||||
}
|
||||
const { commands, items, primary } = button.props || button;
|
||||
if (primary) {
|
||||
enableListener(primary);
|
||||
}
|
||||
if (items) {
|
||||
items.forEach(enableListener);
|
||||
}
|
||||
const hpCommand = commands?.find?.(isHangingProtocolCommand);
|
||||
if (!hpCommand) {
|
||||
return;
|
||||
}
|
||||
const { protocolId, stageIndex, stageId } = hpCommand.commandOptions;
|
||||
const isActive =
|
||||
(!protocolId || protocolId === protocol.id) &&
|
||||
(stageIndex === undefined || stageIndex === toggleStageIndex) &&
|
||||
(!stageId || stageId === stage.id);
|
||||
toolbarService.setToggled(button.id, isActive);
|
||||
};
|
||||
Object.values(toolbarService.getButtons()).forEach(enableListener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the specified protocol
|
||||
* 1. Records any existing state using the viewport grid service
|
||||
@ -173,14 +139,12 @@ const commandsModule = ({
|
||||
stageIndex,
|
||||
reset = false,
|
||||
}: HangingProtocolParams): boolean => {
|
||||
const primaryToolBeforeHPChange = toolbarService.getActivePrimaryTool();
|
||||
try {
|
||||
// Stores in the state the display set selector id to displaySetUID mapping
|
||||
// Pass in viewportId for the active viewport. This item will get set as
|
||||
// the activeViewportId
|
||||
const state = viewportGridService.getState();
|
||||
const hpInfo = hangingProtocolService.getState();
|
||||
const { protocol: oldProtocol } = hangingProtocolService.getActiveProtocol();
|
||||
const stateSyncReduce = reuseCachedLayouts(state, hangingProtocolService, stateSyncService);
|
||||
const { hangingProtocolStageIndexMap, viewportGridStore, displaySetSelectorMap } =
|
||||
stateSyncReduce;
|
||||
@ -243,33 +207,9 @@ const commandsModule = ({
|
||||
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`
|
||||
];
|
||||
stateSyncService.store(stateSyncReduce);
|
||||
// This is a default action applied
|
||||
actions.toggleHpTools();
|
||||
|
||||
// try to use the same tool in the new hanging protocol stage
|
||||
const primaryButton = toolbarService.getButton(primaryToolBeforeHPChange);
|
||||
if (primaryButton) {
|
||||
// is there any type of interaction on this button, if not it might be in the
|
||||
// items. This is a bit of a hack, but it works for now.
|
||||
|
||||
let interactionType = primaryButton.props?.interactionType;
|
||||
|
||||
if (!interactionType && primaryButton.props?.items) {
|
||||
const firstItem = primaryButton.props.items[0];
|
||||
interactionType = firstItem.props?.interactionType || firstItem.props?.type;
|
||||
}
|
||||
|
||||
if (interactionType) {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType,
|
||||
...primaryButton.props,
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.toggleHpTools();
|
||||
uiNotificationService.show({
|
||||
title: 'Apply Hanging Protocol',
|
||||
message: 'The hanging protocol could not be applied.',
|
||||
@ -639,56 +579,38 @@ const commandsModule = ({
|
||||
},
|
||||
clearMeasurements: {
|
||||
commandFn: actions.clearMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
displayNotification: {
|
||||
commandFn: actions.displayNotification,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setHangingProtocol: {
|
||||
commandFn: actions.setHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleHangingProtocol: {
|
||||
commandFn: actions.toggleHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
navigateHistory: {
|
||||
commandFn: actions.navigateHistory,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
nextStage: {
|
||||
commandFn: actions.deltaStage,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousStage: {
|
||||
commandFn: actions.deltaStage,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
setViewportGridLayout: {
|
||||
commandFn: actions.setViewportGridLayout,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleOneUp: {
|
||||
commandFn: actions.toggleOneUp,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
openDICOMTagViewer: {
|
||||
commandFn: actions.openDICOMTagViewer,
|
||||
},
|
||||
updateViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -93,8 +93,8 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
instance && this.attribute
|
||||
? instance[this.attribute]
|
||||
: this.contentF && typeof this.contentF === 'function'
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,39 +1,67 @@
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider';
|
||||
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
|
||||
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
|
||||
import ToolbarButtonWithServices from './Toolbar/ToolbarButtonWithServices';
|
||||
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
|
||||
const getClassName = isToggled => {
|
||||
return {
|
||||
className: isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
};
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const { cineService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButton,
|
||||
},
|
||||
{
|
||||
name: 'ohif.divider',
|
||||
defaultComponent: ToolbarDivider,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.action',
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.splitButton',
|
||||
defaultComponent: ToolbarSplitButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.layoutSelector',
|
||||
defaultComponent: ToolbarLayoutSelectorWithServices,
|
||||
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.toggle',
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
name: 'ohif.buttonGroup',
|
||||
defaultComponent: ToolbarButtonGroupWithServices,
|
||||
},
|
||||
{
|
||||
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: () => {
|
||||
const isToggled = cineService.getState().isCineEnabled;
|
||||
return getClassName(isToggled);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -10,8 +10,13 @@ const metadataProvider = classes.MetadataProvider;
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }): void {
|
||||
const { stateSyncService } = servicesManager.services;
|
||||
export default function init({ servicesManager, configuration = {}, commandsManager }): void {
|
||||
const { stateSyncService, toolbarService, cineService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cineService, [
|
||||
cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
]);
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata);
|
||||
|
||||
@ -24,6 +29,10 @@ export default function init({ servicesManager, configuration = {} }): void {
|
||||
// Used to recover manual changes to the layout of a stage.
|
||||
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||
|
||||
// uiStateStore is a sync state which stores the relevant
|
||||
// UI state for the viewer
|
||||
stateSyncService.register('uiStateStore', { clearOnModeExit: true });
|
||||
|
||||
// displaySetSelectorMap stores a map from
|
||||
// `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to
|
||||
// a displaySetInstanceUID, used to display named display sets in
|
||||
@ -38,7 +47,7 @@ export default function init({ servicesManager, configuration = {} }): void {
|
||||
});
|
||||
|
||||
// Stores a map from the to be applied hanging protocols `<activeStudyUID>:<protocolId>`
|
||||
// to the previously applied hanging protolStageIndexMap key, in order to toggle
|
||||
// to the previously applied hanging protocolStageIndexMap key, in order to toggle
|
||||
// off the applied protocol and remember the old state.
|
||||
stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
|
||||
|
||||
@ -46,6 +55,45 @@ export default function init({ servicesManager, configuration = {} }): void {
|
||||
// changes numRows and numCols, the viewports can be remembers and then replaced
|
||||
// afterwards.
|
||||
stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
|
||||
|
||||
// Function to process and subscribe to events for a given set of commands and listeners
|
||||
const subscribeToEvents = listeners => {
|
||||
Object.entries(listeners).forEach(([event, commands]) => {
|
||||
const supportedEvents = [
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
viewportGridService.EVENTS.VIEWPORTS_READY,
|
||||
];
|
||||
|
||||
if (supportedEvents.includes(event)) {
|
||||
viewportGridService.subscribe(event, eventData => {
|
||||
const viewportId = eventData?.viewportId ?? viewportGridService.getActiveViewportId();
|
||||
|
||||
commandsManager.run(commands, { viewportId });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => {
|
||||
const { buttons } = state;
|
||||
for (const [id, button] of Object.entries(buttons)) {
|
||||
const { groupId, items, listeners } = button.props;
|
||||
|
||||
// Handle group items' listeners
|
||||
if (groupId && items) {
|
||||
items.forEach(item => {
|
||||
if (item.listeners) {
|
||||
subscribeToEvents(item.listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle button listeners
|
||||
if (listeners) {
|
||||
subscribeToEvents(listeners);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
|
||||
@ -11,16 +11,6 @@ import dcmjs from 'dcmjs';
|
||||
import cleanDenaturalizedDataset from './utils/cleanDenaturalizedDataset';
|
||||
import MicroscopyService from './services/MicroscopyService';
|
||||
|
||||
function transformImageTypeUnnaturalized(entry) {
|
||||
if (entry.vr === 'CS') {
|
||||
return {
|
||||
vr: 'US',
|
||||
Value: entry.Value[0].split('\\'),
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
class DicomMicroscopyViewport extends Component {
|
||||
state = {
|
||||
error: null as any,
|
||||
@ -250,7 +240,6 @@ class DicomMicroscopyViewport extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { displaySets, viewportOptions } = this.props;
|
||||
const { viewportId } = viewportOptions;
|
||||
// Todo-rename: this is always getting the 0
|
||||
const displaySet = displaySets[0];
|
||||
this.installOpenLayersRenderer(this.container.current, displaySet).then(() => {
|
||||
|
||||
@ -124,7 +124,7 @@ export default function getCommandsModule({
|
||||
}
|
||||
|
||||
// overview
|
||||
const { activeViewportId, viewports } = viewportGridService.getState();
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
microscopyService.toggleOverviewMap(activeViewportId);
|
||||
},
|
||||
toggleAnnotations: () => {
|
||||
@ -135,28 +135,18 @@ export default function getCommandsModule({
|
||||
const definitions = {
|
||||
deleteMeasurement: {
|
||||
commandFn: actions.deleteMeasurement,
|
||||
storeContexts: [] as any[],
|
||||
options: {},
|
||||
},
|
||||
setLabel: {
|
||||
commandFn: actions.setLabel,
|
||||
storeContexts: [] as any[],
|
||||
options: {},
|
||||
},
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [] as any[],
|
||||
options: {},
|
||||
},
|
||||
toggleOverlays: {
|
||||
commandFn: actions.toggleOverlays,
|
||||
storeContexts: [] as any[],
|
||||
options: {},
|
||||
},
|
||||
toggleAnnotations: {
|
||||
commandFn: actions.toggleAnnotations,
|
||||
storeContexts: [] as any[],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { id } from './id';
|
||||
import React, { Suspense, useMemo } from 'react';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './getCommandsModule';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import getDicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler';
|
||||
@ -23,14 +24,14 @@ const MicroscopyViewport = props => {
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
export default {
|
||||
const extension: Types.Extensions.Extension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
|
||||
async preRegistration({ servicesManager, commandsManager, configuration = {}, appConfig }) {
|
||||
async preRegistration({ servicesManager }) {
|
||||
servicesManager.registerService(MicroscopyService.REGISTRATION(servicesManager));
|
||||
},
|
||||
|
||||
@ -90,6 +91,45 @@ export default {
|
||||
];
|
||||
},
|
||||
|
||||
getToolbarModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.microscopyTool',
|
||||
evaluate: ({ button }) => {
|
||||
const { microscopyService } = servicesManager.services;
|
||||
|
||||
const activeInteractions = microscopyService.getActiveInteractions();
|
||||
|
||||
const isPrimaryActive = activeInteractions.find(interactions => {
|
||||
const sameMouseButton = interactions[1].bindings.mouseButtons.includes('left');
|
||||
|
||||
if (!sameMouseButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notDraw = interactions[0] !== 'draw';
|
||||
|
||||
// there seems to be a custom logic for draw tool for some reason
|
||||
return notDraw
|
||||
? interactions[0] === button.id
|
||||
: interactions[1].geometryType === button.id;
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black bg-primary-light'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
|
||||
// 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,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* SopClassHandlerModule should provide a list of sop class handlers that will be
|
||||
* available in OHIF for Modes to consume and use to create displaySets from Series.
|
||||
@ -113,3 +153,5 @@ export default {
|
||||
|
||||
getCommandsModule,
|
||||
};
|
||||
|
||||
export default extension;
|
||||
|
||||
@ -573,6 +573,10 @@ export default class MicroscopyService extends PubSubService {
|
||||
this.activeInteractions = interactions;
|
||||
}
|
||||
|
||||
getActiveInteractions() {
|
||||
return this.activeInteractions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the relabelling process for the given RoiAnnotation instance, by
|
||||
* publishing the RELABEL event to notify the subscribers
|
||||
|
||||
@ -41,18 +41,6 @@ const dicomPDFExtension = {
|
||||
|
||||
return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }];
|
||||
},
|
||||
// getCommandsModule({ servicesManager }) {
|
||||
// return {
|
||||
// definitions: {
|
||||
// setToolActive: {
|
||||
// commandFn: () => null,
|
||||
// storeContexts: [],
|
||||
// options: {},
|
||||
// },
|
||||
// },
|
||||
// defaultContext: 'ACTIVE_VIEWPORT::PDF',
|
||||
// };
|
||||
// },
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
|
||||
@ -42,19 +42,6 @@ const dicomVideoExtension = {
|
||||
|
||||
return [{ name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }];
|
||||
},
|
||||
// getCommandsModule({ servicesManager }) {
|
||||
// return {
|
||||
// definitions: {
|
||||
// setToolActive: {
|
||||
// commandFn: ({ toolName, element }) => {
|
||||
// },
|
||||
// storeContexts: [],
|
||||
// options: {},
|
||||
// },
|
||||
// },
|
||||
// defaultContext: 'ACTIVE_VIEWPORT::VIDEO',
|
||||
// };
|
||||
// },
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
|
||||
@ -5,6 +5,12 @@
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
// Note: the default width and height of the canvas is 300x150
|
||||
// but we need to set the width and height to the same number since
|
||||
// the thumbnails are usually square and we want to maintain the aspect ratio
|
||||
canvas.width = 128 / window.devicePixelRatio;
|
||||
canvas.height = 128 / window.devicePixelRatio;
|
||||
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas({ canvas, imageId })
|
||||
.then(imageId => {
|
||||
|
||||
@ -330,12 +330,12 @@ function _getStatusComponent(isTracked) {
|
||||
<div className="ml-4 flex">
|
||||
<span className="text-common-light text-base">
|
||||
{isTracked ? (
|
||||
<>
|
||||
{t('Series is tracked and can be viewed in the measurement panel')}
|
||||
</>
|
||||
<>{t('Series is tracked and can be viewed in the measurement panel')}</>
|
||||
) : (
|
||||
<>
|
||||
{t('Measurements for untracked series will not be shown in the measurements panel')}
|
||||
{t(
|
||||
'Measurements for untracked series will not be shown in the measurements panel'
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@ -109,23 +109,6 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
||||
throw new Error('No ptDisplaySet found');
|
||||
}
|
||||
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
|
||||
// Todo: we don't have a proper way to perform a toggle command and update the
|
||||
// state for the toolbar, so here, we manually toggle the toolbar
|
||||
|
||||
// Todo: Crosshairs have bugs for the camera reset currently, so we need to
|
||||
// force turn it off before we update the metadata
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
commandsManager.runCommand('toggleCrosshairs', {
|
||||
toolGroupId,
|
||||
toggledState: false,
|
||||
});
|
||||
});
|
||||
|
||||
toolbarService.state.toggles['Crosshairs'] = false;
|
||||
toolbarService._broadcastEvent(toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED);
|
||||
|
||||
// metadata should be dcmjs naturalized
|
||||
DicomMetadataStore.updateMetadataForSeries(
|
||||
ptDisplaySet.StudyInstanceUID,
|
||||
@ -135,6 +118,12 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
||||
|
||||
// update the displaySets
|
||||
displaySetService.setDisplaySetMetadataInvalidated(ptDisplaySet.displaySetInstanceUID);
|
||||
|
||||
// Crosshair position depends on the metadata values such as the positioning interaction
|
||||
// between series, so when the metadata is updated, the crosshairs need to be reset.
|
||||
setTimeout(() => {
|
||||
commandsManager.runCommand('resetCrosshairs');
|
||||
}, 0);
|
||||
}
|
||||
return (
|
||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||
|
||||
@ -530,83 +530,51 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
||||
const definitions = {
|
||||
setEndSliceForROIThresholdTool: {
|
||||
commandFn: actions.setEndSliceForROIThresholdTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setStartSliceForROIThresholdTool: {
|
||||
commandFn: actions.setStartSliceForROIThresholdTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getMatchingPTDisplaySet: {
|
||||
commandFn: actions.getMatchingPTDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getPTMetadata: {
|
||||
commandFn: actions.getPTMetadata,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createNewLabelmapFromPT: {
|
||||
commandFn: actions.createNewLabelmapFromPT,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setSegmentationActiveForToolGroups: {
|
||||
commandFn: actions.setSegmentationActiveForToolGroups,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
thresholdSegmentationByRectangleROITool: {
|
||||
commandFn: actions.thresholdSegmentationByRectangleROITool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getTotalLesionGlycolysis: {
|
||||
commandFn: actions.getTotalLesionGlycolysis,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
calculateSuvPeak: {
|
||||
commandFn: actions.calculateSuvPeak,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getLesionStats: {
|
||||
commandFn: actions.getLesionStats,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
calculateTMTV: {
|
||||
commandFn: actions.calculateTMTV,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
exportTMTVReportCSV: {
|
||||
commandFn: actions.exportTMTVReportCSV,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createTMTVRTReport: {
|
||||
commandFn: actions.createTMTVRTReport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getSegmentationCSVReport: {
|
||||
commandFn: actions.getSegmentationCSVReport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
exportRTReportForAnnotations: {
|
||||
commandFn: actions.exportRTReportForAnnotations,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setFusionPTColormap: {
|
||||
commandFn: actions.setFusionPTColormap,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
* @param {Object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, extensionManager }) {
|
||||
export default function init({ servicesManager }) {
|
||||
const { measurementService, displaySetService, cornerstoneViewportService } =
|
||||
servicesManager.services;
|
||||
|
||||
@ -38,6 +38,5 @@ export default function init({ servicesManager, extensionManager }) {
|
||||
RectangleROIStartEndThreshold.toAnnotation,
|
||||
RectangleROIStartEndThreshold.toMeasurement
|
||||
);
|
||||
|
||||
colormaps.forEach(registerColormap);
|
||||
}
|
||||
|
||||
@ -89,39 +89,8 @@ function modeFactory({ modeConfiguration }) {
|
||||
// disabled
|
||||
};
|
||||
|
||||
const toolGroupId = 'default';
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
toolGroupService.createToolGroupAndAddTools('default', tools);
|
||||
|
||||
let unsubscribe;
|
||||
|
||||
const activateTool = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// We don't need to reset the active tool whenever a viewport is getting
|
||||
// added to the toolGroup.
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
// Since we only have one viewport for the basic cs3d mode and it has
|
||||
// only one hanging protocol, we can just use the first viewport
|
||||
({ unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
activateTool
|
||||
));
|
||||
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
|
||||
@ -1,47 +1,16 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
// Only ways that you can pass in a custom React component for render :l
|
||||
import {
|
||||
// ExpandableToolbarButton,
|
||||
// ListMenu,
|
||||
WindowLevelMenuItem,
|
||||
} from '@ohif/ui';
|
||||
import { defaults } from '@ohif/core';
|
||||
import { defaults, ToolbarService } from '@ohif/core';
|
||||
import type { Button } from '@ohif/core/types';
|
||||
|
||||
const { windowLevelPresets } = defaults;
|
||||
/**
|
||||
*
|
||||
* @param {*} type - 'tool' | 'action' | 'toggle'
|
||||
* @param {*} id
|
||||
* @param {*} icon
|
||||
* @param {*} label
|
||||
*/
|
||||
function _createButton(type, id, icon, label, commands, tooltip) {
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
type,
|
||||
commands,
|
||||
tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
const _createActionButton = _createButton.bind(null, 'action');
|
||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} preset - preset number (from above import)
|
||||
* @param {*} title
|
||||
* @param {*} subtitle
|
||||
*/
|
||||
function _createWwwcPreset(preset, title, subtitle) {
|
||||
return {
|
||||
id: preset.toString(),
|
||||
title,
|
||||
subtitle,
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setWindowLevel',
|
||||
@ -54,147 +23,91 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
};
|
||||
}
|
||||
|
||||
const toolbarButtons = [
|
||||
// Measurement
|
||||
function _createSetToolActiveCommands(toolName, toolGroupIds = ['default', 'mpr', ]) {
|
||||
return toolGroupIds.map(toolGroupId => ({
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolGroupId,
|
||||
toolName,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
}));
|
||||
}
|
||||
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
isRadio: true, // ?
|
||||
// Switch?
|
||||
primary: _createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Length',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Length'
|
||||
),
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: _createSetToolActiveCommands('Length'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Measure Tools',
|
||||
},
|
||||
items: [
|
||||
_createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Length',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Length Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'Bidirectional',
|
||||
'tool-bidirectional',
|
||||
'Bidirectional',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Bidirectional',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Bidirectional Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'EllipticalROI',
|
||||
'tool-elipse',
|
||||
'Ellipse',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'EllipticalROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Ellipse Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'CircleROI',
|
||||
'tool-circle',
|
||||
'Circle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'CircleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Circle Tool'
|
||||
),
|
||||
ToolbarService.createButton({
|
||||
id: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
label: 'Bidirectional',
|
||||
tooltip: 'Bidirectional Tool',
|
||||
commands: _createSetToolActiveCommands('Bidirectional'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'EllipticalROI',
|
||||
icon: 'tool-ellipse',
|
||||
label: 'Ellipse',
|
||||
tooltip: 'Ellipse ROI',
|
||||
commands: _createSetToolActiveCommands('EllipticalROI'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'CircleROI',
|
||||
icon: 'tool-circle',
|
||||
label: 'Circle',
|
||||
tooltip: 'Circle Tool',
|
||||
commands: _createSetToolActiveCommands('CircleROI'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Zoom..
|
||||
{
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Zoom',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: _createSetToolActiveCommands('Zoom'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Window Level + Presets...
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: _createToolButton(
|
||||
'WindowLevel',
|
||||
'tool-window-level',
|
||||
'Window Level',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Window Level'
|
||||
),
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'WindowLevel',
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
tooltip: 'Window Level',
|
||||
commands: _createSetToolActiveCommands('WindowLevel'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: 'W/L Manual',
|
||||
isActive: true,
|
||||
tooltip: 'W/L Presets',
|
||||
},
|
||||
isAction: true, // ?
|
||||
renderer: WindowLevelMenuItem,
|
||||
items: [
|
||||
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
|
||||
@ -205,157 +118,141 @@ const toolbarButtons = [
|
||||
],
|
||||
},
|
||||
},
|
||||
// Pan...
|
||||
{
|
||||
id: 'Pan',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Pan',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: _createSetToolActiveCommands('Pan'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showDownloadViewportModal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
uiType: 'ohif.layoutSelector',
|
||||
props: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
evaluate: 'evaluate.action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
type: 'ohif.layoutSelector',
|
||||
},
|
||||
// More...
|
||||
{
|
||||
id: 'MoreTools',
|
||||
type: 'ohif.splitButton',
|
||||
props: {
|
||||
isRadio: true, // ?
|
||||
groupId: 'MoreTools',
|
||||
primary: _createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
id: 'MoreTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MoreTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
label: 'Reset View',
|
||||
tooltip: 'Reset View',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService.createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
label: 'Reset View',
|
||||
tooltip: 'Reset View',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'RotateRight',
|
||||
icon: 'tool-rotate-right',
|
||||
label: 'Rotate Right',
|
||||
tooltip: 'Rotate Right +90',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
commandName: 'rotateViewportCW',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
_createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
_createActionButton(
|
||||
'rotate-right',
|
||||
'tool-rotate-right',
|
||||
'Rotate Right',
|
||||
[
|
||||
{
|
||||
commandName: 'rotateViewportCW',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rotate +90'
|
||||
),
|
||||
_createActionButton(
|
||||
'flip-horizontal',
|
||||
'tool-flip-horizontal',
|
||||
'Flip Horizontally',
|
||||
[
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Flip Horizontal'
|
||||
),
|
||||
_createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
'Stack Scroll',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'StackScroll',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Stack Scroll'
|
||||
),
|
||||
_createActionButton(
|
||||
'invert',
|
||||
'tool-invert',
|
||||
'Invert',
|
||||
[
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Invert Colors'
|
||||
),
|
||||
_createToolButton(
|
||||
'CalibrationLine',
|
||||
'tool-calibration',
|
||||
'Calibration',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'CalibrationLine',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Calibration Line'
|
||||
),
|
||||
],
|
||||
},
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'FlipHorizontal',
|
||||
icon: 'tool-flip-horizontal',
|
||||
label: 'Flip Horizontally',
|
||||
tooltip: 'Flip Horizontally',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'StackScroll',
|
||||
icon: 'tool-stack-scroll',
|
||||
label: 'Stack Scroll',
|
||||
tooltip: 'Stack Scroll',
|
||||
commands: _createSetToolActiveCommands('StackScroll'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'Invert',
|
||||
icon: 'tool-invert',
|
||||
label: 'Invert Colors',
|
||||
tooltip: 'Invert Colors',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'CalibrationLine',
|
||||
icon: 'tool-calibration',
|
||||
label: 'Calibration Line',
|
||||
tooltip: 'Calibration Line',
|
||||
commands: _createSetToolActiveCommands('CalibrationLine'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
|
||||
@ -3,16 +3,12 @@ import toolbarButtons from './toolbarButtons';
|
||||
import { id } from './id';
|
||||
import initToolGroups from './initToolGroups';
|
||||
import moreTools from './moreTools';
|
||||
import moreToolsMpr from './moreToolsMpr';
|
||||
import i18n from 'i18next';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
|
||||
|
||||
const DEFAULT_TOOL_GROUP_ID = 'default';
|
||||
const MPR_TOOL_GROUP_ID = 'mpr';
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
@ -82,50 +78,8 @@ function modeFactory() {
|
||||
'@ohif/extension-test.customizationModule.custom-context-menu',
|
||||
]);
|
||||
|
||||
let unsubscribe;
|
||||
toolbarService.setDefaultTool({
|
||||
groupId: 'WindowLevel',
|
||||
itemId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const activateTool = () => {
|
||||
toolbarService.recordInteraction(toolbarService.getDefaultTool());
|
||||
|
||||
// We don't need to reset the active tool whenever a viewport is getting
|
||||
// added to the toolGroup.
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
// Since we only have one viewport for the basic cs3d mode and it has
|
||||
// only one hanging protocol, we can just use the first viewport
|
||||
({ unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
activateTool
|
||||
));
|
||||
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons([...toolbarButtons, ...moreTools, ...moreToolsMpr]);
|
||||
toolbarService.createButtonSection(DEFAULT_TOOL_GROUP_ID, [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'MPR',
|
||||
'MoreTools',
|
||||
]);
|
||||
toolbarService.createButtonSection(MPR_TOOL_GROUP_ID, [
|
||||
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
@ -134,7 +88,7 @@ function modeFactory() {
|
||||
'Layout',
|
||||
'MPR',
|
||||
'Crosshairs',
|
||||
'MoreToolsMpr',
|
||||
'MoreTools',
|
||||
]);
|
||||
},
|
||||
onModeExit: ({ servicesManager }) => {
|
||||
|
||||
@ -1,228 +1,183 @@
|
||||
import type { RunCommand } from '@ohif/core/types';
|
||||
import { EVENTS } from '@cornerstonejs/core';
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
import { ToolbarService, ViewportGridService } from '@ohif/core';
|
||||
import { setToolActiveToolbar } from './toolbarButtons';
|
||||
const { createButton } = ToolbarService;
|
||||
|
||||
const ReferenceLinesCommands: RunCommand = [
|
||||
const ReferenceLinesListeners: RunCommand = [
|
||||
{
|
||||
commandName: 'setSourceViewportForReferenceLinesTool',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
];
|
||||
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'MoreTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
isRadio: true, // ?
|
||||
groupId: 'MoreTools',
|
||||
primary: ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
tooltip: 'Reset View',
|
||||
label: 'Reset',
|
||||
commands: 'resetViewport',
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
label: 'Reset View',
|
||||
tooltip: 'Reset View',
|
||||
commands: 'resetViewport',
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
createButton({
|
||||
id: 'rotate-right',
|
||||
icon: 'tool-rotate-right',
|
||||
label: 'Rotate Right',
|
||||
tooltip: 'Rotate +90',
|
||||
commands: 'rotateViewportCW',
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
createButton({
|
||||
id: 'flipHorizontal',
|
||||
icon: 'tool-flip-horizontal',
|
||||
label: 'Flip Horizontal',
|
||||
tooltip: 'Flip Horizontally',
|
||||
commands: 'flipViewportHorizontal',
|
||||
evaluate: 'evaluate.viewportProperties.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ImageSliceSync',
|
||||
icon: 'link',
|
||||
label: 'Image Slice Sync',
|
||||
tooltip: 'Enable position synchronization on stack viewports',
|
||||
commands: {
|
||||
commandName: 'toggleSynchronizer',
|
||||
commandOptions: {
|
||||
type: 'imageSlice',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'rotate-right',
|
||||
'tool-rotate-right',
|
||||
'Rotate Right',
|
||||
[
|
||||
{
|
||||
commandName: 'rotateViewportCW',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rotate +90'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'flip-horizontal',
|
||||
'tool-flip-horizontal',
|
||||
'Flip Horizontally',
|
||||
[
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Flip Horizontally'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ImageSliceSync',
|
||||
'link',
|
||||
'Image Slice Sync',
|
||||
[
|
||||
{
|
||||
},
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleImageSliceSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
],
|
||||
'Enable position synchronization on stack viewports',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleImageSliceSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.cornerstone.synchronizer',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ReferenceLines',
|
||||
icon: 'tool-referenceLines',
|
||||
label: 'Reference Lines',
|
||||
tooltip: 'Show Reference Lines',
|
||||
commands: {
|
||||
commandName: 'setToolEnabled',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
toggle: true,
|
||||
},
|
||||
}
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
ReferenceLinesCommands,
|
||||
'Show Reference Lines',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
|
||||
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
|
||||
},
|
||||
listeners: {
|
||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||
},
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ImageOverlay',
|
||||
icon: 'toggle-dicom-overlay',
|
||||
label: 'Image Overlay',
|
||||
tooltip: 'Toggle Image Overlay',
|
||||
commands: {
|
||||
commandName: 'setToolEnabled',
|
||||
commandOptions: {
|
||||
toolName: 'ImageOverlayViewer',
|
||||
toggle: true,
|
||||
},
|
||||
}
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
'Stack Scroll',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'StackScroll',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Stack Scroll'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'invert',
|
||||
'tool-invert',
|
||||
'Invert',
|
||||
[
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Invert Colors'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Probe',
|
||||
'tool-probe',
|
||||
'Probe',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'DragProbe',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Probe'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'cine',
|
||||
'tool-cine',
|
||||
'Cine',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Cine'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Angle',
|
||||
'tool-angle',
|
||||
'Angle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Angle',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Angle'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Magnify',
|
||||
'tool-magnify',
|
||||
'Magnify',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Magnify',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Magnify'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Rectangle',
|
||||
'tool-rectangle',
|
||||
'Rectangle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'RectangleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rectangle'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'TagBrowser',
|
||||
'list-bullets',
|
||||
'Dicom Tag Browser',
|
||||
[
|
||||
{
|
||||
commandName: 'openDICOMTagViewer',
|
||||
commandOptions: {},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
'Dicom Tag Browser'
|
||||
),
|
||||
},
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'StackScroll',
|
||||
icon: 'tool-stack-scroll',
|
||||
label: 'Stack Scroll',
|
||||
tooltip: 'Stack Scroll',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'invert',
|
||||
icon: 'tool-invert',
|
||||
label: 'Invert',
|
||||
tooltip: 'Invert Colors',
|
||||
commands: 'invertViewport',
|
||||
evaluate: 'evaluate.viewportProperties.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Probe',
|
||||
icon: 'tool-probe',
|
||||
label: 'Probe',
|
||||
tooltip: 'Probe',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Cine',
|
||||
icon: 'tool-cine',
|
||||
label: 'Cine',
|
||||
tooltip: 'Cine',
|
||||
commands: 'toggleCine',
|
||||
evaluate: 'evaluate.cine',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Angle',
|
||||
icon: 'tool-angle',
|
||||
label: 'Angle',
|
||||
tooltip: 'Angle',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Magnify',
|
||||
icon: 'tool-magnify',
|
||||
label: 'Magnify',
|
||||
tooltip: 'Magnify',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'RectangleROI',
|
||||
icon: 'tool-rectangle',
|
||||
label: 'Rectangle',
|
||||
tooltip: 'Rectangle',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'CalibrationLine',
|
||||
icon: 'tool-calibration',
|
||||
label: 'Calibration',
|
||||
tooltip: 'Calibration Line',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'TagBrowser',
|
||||
icon: 'list-bullets',
|
||||
label: 'Dicom Tag Browser',
|
||||
tooltip: 'Dicom Tag Browser',
|
||||
commands: 'openDICOMTagViewer',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
|
||||
const moreToolsMpr = [
|
||||
{
|
||||
id: 'MoreToolsMpr',
|
||||
type: 'ohif.splitButton',
|
||||
props: {
|
||||
isRadio: true, // ?
|
||||
groupId: 'MoreTools',
|
||||
primary: ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
'Stack Scroll',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'StackScroll',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Stack Scroll'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'invert',
|
||||
'tool-invert',
|
||||
'Invert',
|
||||
[
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Invert Colors'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Probe',
|
||||
'tool-probe',
|
||||
'Probe',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'DragProbe',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Probe'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'cine',
|
||||
'tool-cine',
|
||||
'Cine',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Cine'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Angle',
|
||||
'tool-angle',
|
||||
'Angle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Angle',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Angle'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Rectangle',
|
||||
'tool-rectangle',
|
||||
'Rectangle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'RectangleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rectangle'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'TagBrowser',
|
||||
'list-bullets',
|
||||
'Dicom Tag Browser',
|
||||
[
|
||||
{
|
||||
commandName: 'openDICOMTagViewer',
|
||||
commandOptions: {},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
'Dicom Tag Browser'
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default moreToolsMpr;
|
||||
@ -1,14 +1,14 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
// Only ways that you can pass in a custom React component for render :l
|
||||
import {
|
||||
// ExpandableToolbarButton,
|
||||
// ListMenu,
|
||||
WindowLevelMenuItem,
|
||||
} from '@ohif/ui';
|
||||
import { ToolbarService, defaults } from '@ohif/core';
|
||||
import { defaults, ToolbarService } from '@ohif/core';
|
||||
import type { Button } from '@ohif/core/types';
|
||||
|
||||
const { windowLevelPresets } = defaults;
|
||||
const { createButton } = ToolbarService;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -21,7 +21,6 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
id: preset.toString(),
|
||||
title,
|
||||
subtitle,
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setWindowLevel',
|
||||
@ -34,212 +33,106 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
};
|
||||
}
|
||||
|
||||
export const setToolActiveToolbar = {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||
},
|
||||
};
|
||||
|
||||
const toolbarButtons: Button[] = [
|
||||
// Measurement
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
isRadio: true, // ?
|
||||
// Switch?
|
||||
primary: ToolbarService._createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Length',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRLength',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
// we can use the setToolActive command for this from Cornerstone commandsModule
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Length'
|
||||
),
|
||||
// group evaluate to determine which item should move to the top
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Measure Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService._createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Length',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRLength',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
// we can use the setToolActive command for this from Cornerstone commandsModule
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Length Tool'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Bidirectional',
|
||||
'tool-bidirectional',
|
||||
'Bidirectional',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Bidirectional',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRBidirectional',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Bidirectional Tool'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'ArrowAnnotate',
|
||||
'tool-annotate',
|
||||
'Annotation',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ArrowAnnotate',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRArrowAnnotate',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Arrow Annotate'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'EllipticalROI',
|
||||
'tool-elipse',
|
||||
'Ellipse',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'EllipticalROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SREllipticalROI',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Ellipse Tool'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'CircleROI',
|
||||
'tool-circle',
|
||||
'Circle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'CircleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRCircleROI',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Circle Tool'
|
||||
),
|
||||
createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
label: 'Bidirectional',
|
||||
tooltip: 'Bidirectional Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ArrowAnnotate',
|
||||
icon: 'tool-annotate',
|
||||
label: 'Annotation',
|
||||
tooltip: 'Arrow Annotate',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'EllipticalROI',
|
||||
icon: 'tool-ellipse',
|
||||
label: 'Ellipse',
|
||||
tooltip: 'Ellipse ROI',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'CircleROI',
|
||||
icon: 'tool-circle',
|
||||
label: 'Circle',
|
||||
tooltip: 'Circle Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Zoom..
|
||||
{
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Zoom',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Window Level + Presets...
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: ToolbarService._createToolButton(
|
||||
'WindowLevel',
|
||||
'tool-window-level',
|
||||
'Window Level',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Window Level'
|
||||
),
|
||||
primary: createButton({
|
||||
id: 'WindowLevel',
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
tooltip: 'Window Level',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: 'W/L Manual',
|
||||
isActive: true,
|
||||
tooltip: 'W/L Presets',
|
||||
},
|
||||
isAction: true, // ?
|
||||
renderer: WindowLevelMenuItem,
|
||||
items: [
|
||||
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
|
||||
@ -253,137 +146,47 @@ const toolbarButtons: Button[] = [
|
||||
// Pan...
|
||||
{
|
||||
id: 'Pan',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Pan',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showDownloadViewportModal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.layoutSelector',
|
||||
props: {
|
||||
groupId: 'LayoutTools',
|
||||
isRadio: false,
|
||||
primary: {
|
||||
id: 'Layout',
|
||||
type: 'action',
|
||||
uiType: 'ohif.layoutSelector',
|
||||
icon: 'tool-layout',
|
||||
label: 'Grid Layout',
|
||||
props: {
|
||||
rows: 4,
|
||||
columns: 4,
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setLayout',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'Hanging Protocols',
|
||||
},
|
||||
items: [
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
evaluate: 'evaluate.action',
|
||||
commands: [
|
||||
{
|
||||
id: '2x2',
|
||||
type: 'action',
|
||||
label: '2x2',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid',
|
||||
stageId: '2x2',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3x1',
|
||||
type: 'action',
|
||||
label: '3x1',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid',
|
||||
stageId: '3x1',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2x1',
|
||||
type: 'action',
|
||||
label: '2x1',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid',
|
||||
stageId: '2x1',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1x1',
|
||||
type: 'action',
|
||||
label: '1x1',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: '@ohif/mnGrid',
|
||||
stageId: '1x1',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
commandName: 'setViewportGridLayout',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MPR',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'toggle',
|
||||
icon: 'icon-mpr',
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
@ -394,24 +197,23 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.mpr',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
toolName: 'Crosshairs',
|
||||
},
|
||||
commands: {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['mpr'],
|
||||
},
|
||||
],
|
||||
},
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -2,17 +2,13 @@ import { hotkeys } from '@ohif/core';
|
||||
import i18n from 'i18next';
|
||||
import { id } from './id';
|
||||
import initToolGroups from './initToolGroups';
|
||||
import moreTools from './moreTools';
|
||||
import moreToolsMpr from './moreToolsMpr';
|
||||
import toolbarButtons from './toolbarButtons';
|
||||
import moreTools from './moreTools';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG', 'RTSTRUCT'];
|
||||
|
||||
const DEFAULT_TOOL_GROUP_ID = 'default';
|
||||
const MPR_TOOL_GROUP_ID = 'mpr';
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
@ -75,63 +71,16 @@ function modeFactory({ modeConfiguration }) {
|
||||
* Lifecycle hooks
|
||||
*/
|
||||
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
|
||||
const {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
panelService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const { measurementService, toolbarService, toolGroupService, customizationService } =
|
||||
servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||
|
||||
let unsubscribe;
|
||||
toolbarService.setDefaultTool({
|
||||
groupId: 'WindowLevel',
|
||||
itemId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const activateTool = () => {
|
||||
toolbarService.recordInteraction(toolbarService.getDefaultTool());
|
||||
|
||||
// We don't need to reset the active tool whenever a viewport is getting
|
||||
// added to the toolGroup.
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
// Since we only have one viewport for the basic cs3d mode and it has
|
||||
// only one hanging protocol, we can just use the first viewport
|
||||
({ unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
activateTool
|
||||
));
|
||||
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons([...toolbarButtons, ...moreTools, ...moreToolsMpr]);
|
||||
toolbarService.createButtonSection(DEFAULT_TOOL_GROUP_ID, [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'MPR',
|
||||
'MoreTools',
|
||||
]);
|
||||
toolbarService.createButtonSection(MPR_TOOL_GROUP_ID, [
|
||||
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
@ -140,7 +89,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
'Layout',
|
||||
'MPR',
|
||||
'Crosshairs',
|
||||
'MoreToolsMpr',
|
||||
'MoreTools',
|
||||
]);
|
||||
|
||||
customizationService.addModeCustomizations([
|
||||
@ -176,7 +125,6 @@ function modeFactory({ modeConfiguration }) {
|
||||
const {
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
toolbarService,
|
||||
segmentationService,
|
||||
cornerstoneViewportService,
|
||||
uiDialogService,
|
||||
|
||||
@ -54,15 +54,13 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
|
||||
{ toolName: toolNames.CalibrationLine },
|
||||
],
|
||||
// enabled
|
||||
enabled: [{ toolName: toolNames.ImageOverlayViewer }],
|
||||
// disabled
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }],
|
||||
enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }],
|
||||
};
|
||||
|
||||
toolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
function initSRToolGroup(extensionManager, toolGroupService, commandsManager) {
|
||||
function initSRToolGroup(extensionManager, toolGroupService) {
|
||||
const SRUtilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone-dicom-sr.utilityModule.tools'
|
||||
);
|
||||
@ -186,6 +184,7 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
|
||||
toolName: toolNames.Crosshairs,
|
||||
configuration: {
|
||||
viewportIndicators: false,
|
||||
disableOnPassive: true,
|
||||
autoPan: {
|
||||
enabled: false,
|
||||
panSize: 10,
|
||||
@ -230,7 +229,7 @@ function initVolume3DToolGroup(extensionManager, toolGroupService) {
|
||||
|
||||
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
|
||||
initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default');
|
||||
initSRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||
initSRToolGroup(extensionManager, toolGroupService);
|
||||
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||
initVolume3DToolGroup(extensionManager, toolGroupService);
|
||||
}
|
||||
|
||||
@ -1,295 +1,183 @@
|
||||
import type { RunCommand } from '@ohif/core/types';
|
||||
import { EVENTS } from '@cornerstonejs/core';
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
import { ToolbarService, ViewportGridService } from '@ohif/core';
|
||||
import { setToolActiveToolbar } from './toolbarButtons';
|
||||
const { createButton } = ToolbarService;
|
||||
|
||||
const ReferenceLinesCommands: RunCommand = [
|
||||
const ReferenceLinesListeners: RunCommand = [
|
||||
{
|
||||
commandName: 'setSourceViewportForReferenceLinesTool',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
];
|
||||
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'MoreTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
isRadio: true, // ?
|
||||
groupId: 'MoreTools',
|
||||
primary: ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
tooltip: 'Reset View',
|
||||
label: 'Reset',
|
||||
commands: 'resetViewport',
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
label: 'Reset View',
|
||||
tooltip: 'Reset View',
|
||||
commands: 'resetViewport',
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
createButton({
|
||||
id: 'rotate-right',
|
||||
icon: 'tool-rotate-right',
|
||||
label: 'Rotate Right',
|
||||
tooltip: 'Rotate +90',
|
||||
commands: 'rotateViewportCW',
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
createButton({
|
||||
id: 'flipHorizontal',
|
||||
icon: 'tool-flip-horizontal',
|
||||
label: 'Flip Horizontal',
|
||||
tooltip: 'Flip Horizontally',
|
||||
commands: 'flipViewportHorizontal',
|
||||
evaluate: 'evaluate.viewportProperties.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ImageSliceSync',
|
||||
icon: 'link',
|
||||
label: 'Image Slice Sync',
|
||||
tooltip: 'Enable position synchronization on stack viewports',
|
||||
commands: {
|
||||
commandName: 'toggleSynchronizer',
|
||||
commandOptions: {
|
||||
type: 'imageSlice',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'rotate-right',
|
||||
'tool-rotate-right',
|
||||
'Rotate Right',
|
||||
[
|
||||
{
|
||||
commandName: 'rotateViewportCW',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rotate +90'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'flip-horizontal',
|
||||
'tool-flip-horizontal',
|
||||
'Flip Horizontally',
|
||||
[
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Flip Horizontal'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ImageSliceSync',
|
||||
'link',
|
||||
'Image Slice Sync',
|
||||
[
|
||||
{
|
||||
},
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleImageSliceSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
],
|
||||
'Enable position synchronization on stack viewports',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleImageSliceSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.cornerstone.synchronizer',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ReferenceLines',
|
||||
icon: 'tool-referenceLines',
|
||||
label: 'Reference Lines',
|
||||
tooltip: 'Show Reference Lines',
|
||||
commands: {
|
||||
commandName: 'setToolEnabled',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
toggle: true, // Toggle the tool on/off upon click
|
||||
},
|
||||
}
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
ReferenceLinesCommands,
|
||||
'Show Reference Lines',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
|
||||
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
|
||||
},
|
||||
listeners: {
|
||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||
},
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ImageOverlay',
|
||||
icon: 'toggle-dicom-overlay',
|
||||
label: 'Image Overlay',
|
||||
tooltip: 'Toggle Image Overlay',
|
||||
commands: {
|
||||
commandName: 'setToolEnabled',
|
||||
commandOptions: {
|
||||
toolName: 'ImageOverlayViewer',
|
||||
toggle: true, // Toggle the tool on/off upon click
|
||||
},
|
||||
}
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ImageOverlayViewer',
|
||||
'toggle-dicom-overlay',
|
||||
'Image Overlay',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ImageOverlayViewer',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Image Overlay',
|
||||
{ isActive: true }
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
'Stack Scroll',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'StackScroll',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Stack Scroll'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'invert',
|
||||
'tool-invert',
|
||||
'Invert',
|
||||
[
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Invert Colors'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Probe',
|
||||
'tool-probe',
|
||||
'Probe',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'DragProbe',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Probe'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'cine',
|
||||
'tool-cine',
|
||||
'Cine',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Cine'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Angle',
|
||||
'tool-angle',
|
||||
'Angle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Angle',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Angle'
|
||||
),
|
||||
|
||||
// Next two tools can be added once icons are added
|
||||
// ToolbarService._createToolButton(
|
||||
// 'Cobb Angle',
|
||||
// 'tool-cobb-angle',
|
||||
// 'Cobb Angle',
|
||||
// [
|
||||
// {
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: {
|
||||
// toolName: 'CobbAngle',
|
||||
// },
|
||||
// context: 'CORNERSTONE',
|
||||
// },
|
||||
// ],
|
||||
// 'Cobb Angle'
|
||||
// ),
|
||||
// ToolbarService._createToolButton(
|
||||
// 'Planar Freehand ROI',
|
||||
// 'tool-freehand',
|
||||
// 'PlanarFreehandROI',
|
||||
// [
|
||||
// {
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: {
|
||||
// toolName: 'PlanarFreehandROI',
|
||||
// },
|
||||
// context: 'CORNERSTONE',
|
||||
// },
|
||||
// ],
|
||||
// 'Planar Freehand ROI'
|
||||
// ),
|
||||
ToolbarService._createToolButton(
|
||||
'Magnify',
|
||||
'tool-magnify',
|
||||
'Magnify',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Magnify',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Magnify'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Rectangle',
|
||||
'tool-rectangle',
|
||||
'Rectangle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'RectangleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rectangle'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'CalibrationLine',
|
||||
'tool-calibration',
|
||||
'Calibration',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'CalibrationLine',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Calibration Line'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'TagBrowser',
|
||||
'list-bullets',
|
||||
'Dicom Tag Browser',
|
||||
[
|
||||
{
|
||||
commandName: 'openDICOMTagViewer',
|
||||
commandOptions: {},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
'Dicom Tag Browser'
|
||||
),
|
||||
},
|
||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'StackScroll',
|
||||
icon: 'tool-stack-scroll',
|
||||
label: 'Stack Scroll',
|
||||
tooltip: 'Stack Scroll',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'invert',
|
||||
icon: 'tool-invert',
|
||||
label: 'Invert',
|
||||
tooltip: 'Invert Colors',
|
||||
commands: 'invertViewport',
|
||||
evaluate: 'evaluate.viewportProperties.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Probe',
|
||||
icon: 'tool-probe',
|
||||
label: 'Probe',
|
||||
tooltip: 'Probe',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Cine',
|
||||
icon: 'tool-cine',
|
||||
label: 'Cine',
|
||||
tooltip: 'Cine',
|
||||
commands: 'toggleCine',
|
||||
evaluate: 'evaluate.cine',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Angle',
|
||||
icon: 'tool-angle',
|
||||
label: 'Angle',
|
||||
tooltip: 'Angle',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Magnify',
|
||||
icon: 'tool-magnify',
|
||||
label: 'Magnify',
|
||||
tooltip: 'Magnify',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'RectangleROI',
|
||||
icon: 'tool-rectangle',
|
||||
label: 'Rectangle',
|
||||
tooltip: 'Rectangle',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'CalibrationLine',
|
||||
icon: 'tool-calibration',
|
||||
label: 'Calibration',
|
||||
tooltip: 'Calibration Line',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'TagBrowser',
|
||||
icon: 'list-bullets',
|
||||
label: 'Dicom Tag Browser',
|
||||
tooltip: 'Dicom Tag Browser',
|
||||
commands: 'openDICOMTagViewer',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,242 +0,0 @@
|
||||
import type { RunCommand } from '@ohif/core/types';
|
||||
import { EVENTS } from '@cornerstonejs/core';
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
|
||||
const ReferenceLinesCommands: RunCommand = [
|
||||
{
|
||||
commandName: 'setSourceViewportForReferenceLinesTool',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ReferenceLines',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
];
|
||||
|
||||
const moreToolsMpr = [
|
||||
{
|
||||
id: 'MoreToolsMpr',
|
||||
type: 'ohif.splitButton',
|
||||
props: {
|
||||
isRadio: true, // ?
|
||||
groupId: 'MoreTools',
|
||||
primary: ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService._createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ImageSliceSync',
|
||||
'link',
|
||||
'Image Slice Sync',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleImageSliceSync',
|
||||
},
|
||||
],
|
||||
'Enable position synchronization on stack viewports',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
|
||||
commandName: 'toggleImageSliceSync',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
ReferenceLinesCommands,
|
||||
'Show Reference Lines',
|
||||
{
|
||||
listeners: {
|
||||
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
|
||||
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
|
||||
},
|
||||
}
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'ImageOverlayViewer',
|
||||
'toggle-dicom-overlay',
|
||||
'Image Overlay',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ImageOverlayViewer',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Image Overlay',
|
||||
{ isActive: true }
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
'Stack Scroll',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'StackScroll',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Stack Scroll'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'invert',
|
||||
'tool-invert',
|
||||
'Invert',
|
||||
[
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Invert Colors'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Probe',
|
||||
'tool-probe',
|
||||
'Probe',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'DragProbe',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Probe'
|
||||
),
|
||||
ToolbarService._createToggleButton(
|
||||
'cine',
|
||||
'tool-cine',
|
||||
'Cine',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Cine'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Angle',
|
||||
'tool-angle',
|
||||
'Angle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Angle',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Angle'
|
||||
),
|
||||
|
||||
// Next two tools can be added once icons are added
|
||||
// ToolbarService._createToolButton(
|
||||
// 'Cobb Angle',
|
||||
// 'tool-cobb-angle',
|
||||
// 'Cobb Angle',
|
||||
// [
|
||||
// {
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: {
|
||||
// toolName: 'CobbAngle',
|
||||
// },
|
||||
// context: 'CORNERSTONE',
|
||||
// },
|
||||
// ],
|
||||
// 'Cobb Angle'
|
||||
// ),
|
||||
// ToolbarService._createToolButton(
|
||||
// 'Planar Freehand ROI',
|
||||
// 'tool-freehand',
|
||||
// 'PlanarFreehandROI',
|
||||
// [
|
||||
// {
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: {
|
||||
// toolName: 'PlanarFreehandROI',
|
||||
// },
|
||||
// context: 'CORNERSTONE',
|
||||
// },
|
||||
// ],
|
||||
// 'Planar Freehand ROI'
|
||||
// ),
|
||||
ToolbarService._createToolButton(
|
||||
'Rectangle',
|
||||
'tool-rectangle',
|
||||
'Rectangle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'RectangleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rectangle'
|
||||
),
|
||||
ToolbarService._createActionButton(
|
||||
'TagBrowser',
|
||||
'list-bullets',
|
||||
'Dicom Tag Browser',
|
||||
[
|
||||
{
|
||||
commandName: 'openDICOMTagViewer',
|
||||
commandOptions: {},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
'Dicom Tag Browser'
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default moreToolsMpr;
|
||||
@ -1,7 +1,6 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
// Only ways that you can pass in a custom React component for render :l
|
||||
import {
|
||||
// ExpandableToolbarButton,
|
||||
// ListMenu,
|
||||
WindowLevelMenuItem,
|
||||
} from '@ohif/ui';
|
||||
@ -9,6 +8,7 @@ import { defaults, ToolbarService } from '@ohif/core';
|
||||
import type { Button } from '@ohif/core/types';
|
||||
|
||||
const { windowLevelPresets } = defaults;
|
||||
const { createButton } = ToolbarService;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -21,7 +21,6 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
id: preset.toString(),
|
||||
title,
|
||||
subtitle,
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setWindowLevel',
|
||||
@ -34,224 +33,106 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
};
|
||||
}
|
||||
|
||||
const toolGroupIds = ['default', 'mpr', 'SRToolGroup'];
|
||||
|
||||
/**
|
||||
* Creates an array of 'setToolActive' commands for the given toolName - one for
|
||||
* each toolGroupId specified in toolGroupIds.
|
||||
* @param {string} toolName
|
||||
* @returns {Array} an array of 'setToolActive' commands
|
||||
*/
|
||||
function _createSetToolActiveCommands(toolName) {
|
||||
const temp = toolGroupIds.map(toolGroupId => ({
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolGroupId,
|
||||
toolName,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
}));
|
||||
return temp;
|
||||
}
|
||||
export const setToolActiveToolbar = {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||
},
|
||||
};
|
||||
|
||||
const toolbarButtons: Button[] = [
|
||||
// Measurement
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
isRadio: true, // ?
|
||||
// Switch?
|
||||
primary: ToolbarService._createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Length',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRLength',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
// we can use the setToolActive command for this from Cornerstone commandsModule
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Length'
|
||||
),
|
||||
// group evaluate to determine which item should move to the top
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Measure Tools',
|
||||
},
|
||||
items: [
|
||||
ToolbarService._createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Length',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRLength',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
// we can use the setToolActive command for this from Cornerstone commandsModule
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Length Tool'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'Bidirectional',
|
||||
'tool-bidirectional',
|
||||
'Bidirectional',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Bidirectional',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRBidirectional',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Bidirectional Tool'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'ArrowAnnotate',
|
||||
'tool-annotate',
|
||||
'Annotation',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'ArrowAnnotate',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRArrowAnnotate',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Arrow Annotate'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'EllipticalROI',
|
||||
'tool-elipse',
|
||||
'Ellipse',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'EllipticalROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SREllipticalROI',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Ellipse Tool'
|
||||
),
|
||||
ToolbarService._createToolButton(
|
||||
'CircleROI',
|
||||
'tool-circle',
|
||||
'Circle',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'CircleROI',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'SRCircleROI',
|
||||
toolGroupId: 'SRToolGroup',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Circle Tool'
|
||||
),
|
||||
createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
label: 'Bidirectional',
|
||||
tooltip: 'Bidirectional Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'ArrowAnnotate',
|
||||
icon: 'tool-annotate',
|
||||
label: 'Annotation',
|
||||
tooltip: 'Arrow Annotate',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'EllipticalROI',
|
||||
icon: 'tool-ellipse',
|
||||
label: 'Ellipse',
|
||||
tooltip: 'Ellipse ROI',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'CircleROI',
|
||||
icon: 'tool-circle',
|
||||
label: 'Circle',
|
||||
tooltip: 'Circle Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Zoom..
|
||||
{
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: _createSetToolActiveCommands('Zoom'),
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Window Level + Presets...
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: ToolbarService._createToolButton(
|
||||
'WindowLevel',
|
||||
'tool-window-level',
|
||||
'Window Level',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Window Level'
|
||||
),
|
||||
primary: createButton({
|
||||
id: 'WindowLevel',
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
tooltip: 'Window Level',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: 'W/L Manual',
|
||||
isActive: true,
|
||||
tooltip: 'W/L Presets',
|
||||
},
|
||||
isAction: true, // ?
|
||||
renderer: WindowLevelMenuItem,
|
||||
items: [
|
||||
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
|
||||
@ -265,43 +146,47 @@ const toolbarButtons: Button[] = [
|
||||
// Pan...
|
||||
{
|
||||
id: 'Pan',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: _createSetToolActiveCommands('Pan'),
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showDownloadViewportModal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
uiType: 'ohif.layoutSelector',
|
||||
props: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
evaluate: 'evaluate.action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
type: 'ohif.layoutSelector',
|
||||
props: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MPR',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'toggle',
|
||||
icon: 'icon-mpr',
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
@ -310,31 +195,27 @@ const toolbarButtons: Button[] = [
|
||||
commandOptions: {
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.mpr',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Crosshairs',
|
||||
toolGroupId: 'mpr',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
commands: {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: ['mpr'],
|
||||
},
|
||||
],
|
||||
},
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// More...
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
|
||||
@ -50,7 +50,6 @@ function modeFactory({ modeConfiguration }) {
|
||||
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.createButtonSection('primary', ['MeasurementTools', 'dragPan']);
|
||||
},
|
||||
|
||||
@ -1,182 +1,148 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
/**
|
||||
*
|
||||
* @param {*} type - 'tool' | 'action' | 'toggle'
|
||||
* @param {*} id
|
||||
* @param {*} icon
|
||||
* @param {*} label
|
||||
*/
|
||||
function _createButton(type, id, icon, label, commands, tooltip) {
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
type,
|
||||
commands,
|
||||
tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
const _createActionButton = _createButton.bind(null, 'action');
|
||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
import { ToolbarService } from '@ohif/core';
|
||||
|
||||
const toolbarButtons = [
|
||||
// Measurement
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
isRadio: true, // ?
|
||||
// Switch?
|
||||
primary: _createToolButton(
|
||||
'line',
|
||||
'tool-length',
|
||||
'Line',
|
||||
[
|
||||
// group evaluate to determine which item should move to the top
|
||||
evaluate: 'evaluate.group.promoteToPrimary',
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'line',
|
||||
icon: 'tool-length',
|
||||
label: 'Line',
|
||||
tooltip: 'Line',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'line',
|
||||
},
|
||||
commandOptions: { toolName: 'line' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Line'
|
||||
),
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Measure Tools',
|
||||
},
|
||||
items: [
|
||||
_createToolButton(
|
||||
'line',
|
||||
'tool-length',
|
||||
'Line',
|
||||
[
|
||||
ToolbarService.createButton({
|
||||
id: 'line',
|
||||
icon: 'tool-length',
|
||||
label: 'Line',
|
||||
tooltip: 'Line',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'line',
|
||||
},
|
||||
commandOptions: { toolName: 'line' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Line Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'point',
|
||||
'tool-point',
|
||||
'Point',
|
||||
[
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'point',
|
||||
icon: 'tool-point',
|
||||
label: 'Point',
|
||||
tooltip: 'Point Tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'point',
|
||||
},
|
||||
commandOptions: { toolName: 'point' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Point Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'polygon',
|
||||
'tool-polygon',
|
||||
'Polygon',
|
||||
[
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
// Point Tool was previously defined
|
||||
ToolbarService.createButton({
|
||||
id: 'polygon',
|
||||
icon: 'tool-polygon',
|
||||
label: 'Polygon',
|
||||
tooltip: 'Polygon Tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'polygon',
|
||||
},
|
||||
commandOptions: { toolName: 'polygon' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Polygon Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'circle',
|
||||
'tool-circle',
|
||||
'Circle',
|
||||
[
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'circle',
|
||||
icon: 'tool-circle',
|
||||
label: 'Circle',
|
||||
tooltip: 'Circle Tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'circle',
|
||||
},
|
||||
commandOptions: { toolName: 'circle' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Circle Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'box',
|
||||
'tool-rectangle',
|
||||
'Box',
|
||||
[
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'box',
|
||||
icon: 'tool-rectangle',
|
||||
label: 'Box',
|
||||
tooltip: 'Box Tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'box',
|
||||
},
|
||||
commandOptions: { toolName: 'box' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Box Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'freehandpolygon',
|
||||
'tool-freehand-polygon',
|
||||
'Freehand Polygon',
|
||||
[
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'freehandpolygon',
|
||||
icon: 'tool-freehand-polygon',
|
||||
label: 'Freehand Polygon',
|
||||
tooltip: 'Freehand Polygon Tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'freehandpolygon',
|
||||
},
|
||||
commandOptions: { toolName: 'freehandpolygon' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Freehand Polygon Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'freehandline',
|
||||
'tool-freehand-line',
|
||||
'Freehand Line',
|
||||
[
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'freehandline',
|
||||
icon: 'tool-freehand-line',
|
||||
label: 'Freehand Line',
|
||||
tooltip: 'Freehand Line Tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'freehandline',
|
||||
},
|
||||
commandOptions: { toolName: 'freehandline' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
'Freehand Line Tool'
|
||||
),
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Pan...
|
||||
{
|
||||
id: 'dragPan',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'dragPan',
|
||||
},
|
||||
commandOptions: { toolName: 'dragPan' },
|
||||
context: 'MICROSCOPY',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.microscopyTool',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { id } from './id';
|
||||
import toolbarButtons from './toolbarButtons';
|
||||
import segmentationButtons from './segmentationButtons';
|
||||
import initToolGroups from './initToolGroups';
|
||||
|
||||
const ohif = {
|
||||
@ -57,47 +58,19 @@ function modeFactory({ modeConfiguration }) {
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||
|
||||
let unsubscribe;
|
||||
|
||||
const activateTool = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// We don't need to reset the active tool whenever a viewport is getting
|
||||
// added to the toolGroup.
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
// Since we only have one viewport for the basic cs3d mode and it has
|
||||
// only one hanging protocol, we can just use the first viewport
|
||||
({ unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
activateTool
|
||||
));
|
||||
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.addButtons(segmentationButtons);
|
||||
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'MPR',
|
||||
'Crosshairs',
|
||||
'Zoom',
|
||||
'MoreTools',
|
||||
]);
|
||||
toolbarService.createButtonSection('segmentationToolbox', ['BrushTools', 'Shapes']);
|
||||
},
|
||||
onModeExit: ({ servicesManager }) => {
|
||||
const {
|
||||
|
||||
@ -41,11 +41,7 @@ function createTools(utilityModule) {
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: [-500, 500],
|
||||
},
|
||||
},
|
||||
dynamicRadius: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -53,11 +49,7 @@ function createTools(utilityModule) {
|
||||
parentTool: 'Brush',
|
||||
configuration: {
|
||||
activeStrategy: 'THRESHOLD_INSIDE_SPHERE',
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: [-500, 500],
|
||||
},
|
||||
},
|
||||
dynamicRadius: 3,
|
||||
},
|
||||
},
|
||||
{ toolName: toolNames.CircleScissors },
|
||||
|
||||
195
modes/segmentation/src/segmentationButtons.ts
Normal file
195
modes/segmentation/src/segmentationButtons.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import type { Button } from '@ohif/core/types';
|
||||
|
||||
function _createSetToolActiveCommands(toolName) {
|
||||
return [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
items: [
|
||||
{
|
||||
id: 'Brush',
|
||||
icon: 'icon-tool-brush',
|
||||
label: 'Brush',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||
},
|
||||
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'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
type: 'radio',
|
||||
id: 'brush-mode',
|
||||
value: 'CircularBrush',
|
||||
values: [
|
||||
{ value: 'CircularBrush', label: 'Circle' },
|
||||
{ value: 'SphereBrush', label: 'Sphere' },
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
icon: 'icon-tool-eraser',
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: {
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
},
|
||||
commands: _createSetToolActiveCommands('CircularEraser'),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'eraser-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
step: 0.5,
|
||||
value: 25,
|
||||
commands: {
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: { toolNames: ['CircularEraser', 'SphereEraser'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: 'CircularEraser',
|
||||
values: [
|
||||
{ value: 'CircularEraser', label: 'Circle' },
|
||||
{ value: 'SphereEraser', label: 'Sphere' },
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Threshold',
|
||||
icon: 'icon-tool-threshold',
|
||||
label: 'Eraser',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
options: { toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'] },
|
||||
},
|
||||
commands: _createSetToolActiveCommands('ThresholdCircularBrush'),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'threshold-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
step: 0.5,
|
||||
value: 25,
|
||||
commands: {
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: {
|
||||
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: 'ThresholdCircularBrush',
|
||||
values: [
|
||||
{ value: 'ThresholdCircularBrush', label: 'Circle' },
|
||||
{ value: 'ThresholdSphereBrush', label: 'Sphere' },
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
{
|
||||
name: 'Threshold',
|
||||
type: 'radio',
|
||||
id: 'dynamic-mode',
|
||||
value: 'ThresholdRange',
|
||||
values: [
|
||||
{ value: 'ThresholdDynamic', label: 'Dynamic' },
|
||||
{ value: 'ThresholdRange', label: 'Range' },
|
||||
],
|
||||
commands: {
|
||||
commandName: 'toggleThresholdRangeAndDynamic',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ThresholdRange',
|
||||
type: 'double-range',
|
||||
id: 'threshold-range',
|
||||
min: -1000,
|
||||
max: 1000,
|
||||
step: 1,
|
||||
values: [100, 600],
|
||||
condition: ({ options }) =>
|
||||
options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange',
|
||||
commands: {
|
||||
commandName: 'setThresholdRange',
|
||||
commandOptions: {
|
||||
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
@ -1,46 +1,14 @@
|
||||
import {
|
||||
// ExpandableToolbarButton,
|
||||
// ListMenu,
|
||||
WindowLevelMenuItem,
|
||||
} from '@ohif/ui';
|
||||
import { defaults } from '@ohif/core';
|
||||
import { defaults, ToolbarService } from '@ohif/core';
|
||||
import type { Button } from '@ohif/core/types';
|
||||
import { WindowLevelMenuItem } from '@ohif/ui';
|
||||
|
||||
const { windowLevelPresets } = defaults;
|
||||
/**
|
||||
*
|
||||
* @param {*} type - 'tool' | 'action' | 'toggle'
|
||||
* @param {*} id
|
||||
* @param {*} icon
|
||||
* @param {*} label
|
||||
*/
|
||||
function _createButton(type, id, icon, label, commands, tooltip, uiType) {
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
type,
|
||||
commands,
|
||||
tooltip,
|
||||
uiType,
|
||||
};
|
||||
}
|
||||
|
||||
const _createActionButton = _createButton.bind(null, 'action');
|
||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} preset - preset number (from above import)
|
||||
* @param {*} title
|
||||
* @param {*} subtitle
|
||||
*/
|
||||
function _createWwwcPreset(preset, title, subtitle) {
|
||||
return {
|
||||
id: preset.toString(),
|
||||
title,
|
||||
subtitle,
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setWindowLevel',
|
||||
@ -53,16 +21,8 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
};
|
||||
}
|
||||
|
||||
const toolGroupIds = ['default', 'mpr', 'SRToolGroup'];
|
||||
|
||||
/**
|
||||
* Creates an array of 'setToolActive' commands for the given toolName - one for
|
||||
* each toolGroupId specified in toolGroupIds.
|
||||
* @param {string} toolName
|
||||
* @returns {Array} an array of 'setToolActive' commands
|
||||
*/
|
||||
function _createSetToolActiveCommands(toolName) {
|
||||
const temp = toolGroupIds.map(toolGroupId => ({
|
||||
function _createSetToolActiveCommands(toolName, toolGroupIds = ['default', 'mpr', 'SRToolGroup']) {
|
||||
return toolGroupIds.map(toolGroupId => ({
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolGroupId,
|
||||
@ -70,49 +30,36 @@ function _createSetToolActiveCommands(toolName) {
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
}));
|
||||
return temp;
|
||||
}
|
||||
|
||||
const toolbarButtons = [
|
||||
// Zoom..
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: _createSetToolActiveCommands('Zoom'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Window Level + Presets...
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: _createToolButton(
|
||||
'WindowLevel',
|
||||
'tool-window-level',
|
||||
'Window Level',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Window Level'
|
||||
),
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'WindowLevel',
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
tooltip: 'Window Level',
|
||||
commands: _createSetToolActiveCommands('WindowLevel'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: 'W/L Manual',
|
||||
isActive: true,
|
||||
tooltip: 'W/L Presets',
|
||||
},
|
||||
isAction: true, // ?
|
||||
renderer: WindowLevelMenuItem,
|
||||
items: [
|
||||
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
|
||||
@ -123,46 +70,49 @@ const toolbarButtons = [
|
||||
],
|
||||
},
|
||||
},
|
||||
// Pan...
|
||||
{
|
||||
id: 'Pan',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: _createSetToolActiveCommands('Pan'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showDownloadViewportModal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
uiType: 'ohif.layoutSelector',
|
||||
props: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
evaluate: 'evaluate.action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
type: 'ohif.layoutSelector',
|
||||
props: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MPR',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'toggle',
|
||||
icon: 'icon-mpr',
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
@ -174,180 +124,124 @@ const toolbarButtons = [
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.mpr',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Crosshairs',
|
||||
toolGroupId: 'mpr',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: _createSetToolActiveCommands('Crosshairs', ['mpr']),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// More...
|
||||
{
|
||||
id: 'MoreTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
isRadio: true, // ?
|
||||
groupId: 'MoreTools',
|
||||
primary: _createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'Reset',
|
||||
icon: 'tool-reset',
|
||||
label: 'Reset View',
|
||||
tooltip: 'Reset',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Tools',
|
||||
},
|
||||
items: [
|
||||
_createActionButton(
|
||||
'Reset',
|
||||
'tool-reset',
|
||||
'Reset View',
|
||||
[
|
||||
{
|
||||
commandName: 'resetViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Reset'
|
||||
),
|
||||
_createActionButton(
|
||||
'rotate-right',
|
||||
'tool-rotate-right',
|
||||
'Rotate Right',
|
||||
[
|
||||
ToolbarService.createButton({
|
||||
id: 'RotateRight',
|
||||
icon: 'tool-rotate-right',
|
||||
label: 'Rotate Right',
|
||||
tooltip: 'Rotate +90',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'rotateViewportCW',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Rotate +90'
|
||||
),
|
||||
_createActionButton(
|
||||
'flip-horizontal',
|
||||
'tool-flip-horizontal',
|
||||
'Flip Horizontally',
|
||||
[
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'FlipHorizontal',
|
||||
icon: 'tool-flip-horizontal',
|
||||
label: 'Flip Horizontally',
|
||||
tooltip: 'Flip Horizontal',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Flip Horizontal'
|
||||
),
|
||||
_createToggleButton('ImageSliceSync', 'link', 'Stack Image Sync', [
|
||||
{
|
||||
commandName: 'toggleImageSliceSync',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]),
|
||||
_createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleReferenceLines',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]
|
||||
),
|
||||
_createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
'Stack Scroll',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'StackScroll',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Stack Scroll'
|
||||
),
|
||||
_createActionButton(
|
||||
'invert',
|
||||
'tool-invert',
|
||||
'Invert',
|
||||
[
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'StackScroll',
|
||||
icon: 'tool-stack-scroll',
|
||||
label: 'Stack Scroll',
|
||||
tooltip: 'Stack Scroll',
|
||||
commands: _createSetToolActiveCommands('StackScroll'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'Magnify',
|
||||
icon: 'tool-magnify',
|
||||
label: 'Magnify',
|
||||
tooltip: 'Magnify',
|
||||
commands: _createSetToolActiveCommands('Magnify'),
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'Invert',
|
||||
icon: 'tool-invert',
|
||||
label: 'Invert',
|
||||
tooltip: 'Invert Colors',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'invertViewport',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Invert Colors'
|
||||
),
|
||||
_createToggleButton(
|
||||
'cine',
|
||||
'tool-cine',
|
||||
'Cine',
|
||||
[
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'Cine',
|
||||
icon: 'tool-cine',
|
||||
label: 'Cine',
|
||||
tooltip: 'Cine',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Cine'
|
||||
),
|
||||
_createToolButton(
|
||||
'Magnify',
|
||||
'tool-magnify',
|
||||
'Magnify',
|
||||
[
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Magnify',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
'Magnify'
|
||||
),
|
||||
_createActionButton(
|
||||
'TagBrowser',
|
||||
'list-bullets',
|
||||
'Dicom Tag Browser',
|
||||
[
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'DicomTagBrowser',
|
||||
icon: 'list-bullets',
|
||||
label: 'Dicom Tag Browser',
|
||||
tooltip: 'Dicom Tag Browser',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'openDICOMTagViewer',
|
||||
commandOptions: {},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
'Dicom Tag Browser'
|
||||
),
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -56,39 +56,6 @@ function modeFactory({ modeConfiguration }) {
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(toolNames, Enums, toolGroupService, commandsManager);
|
||||
|
||||
const setWindowLevelActive = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: toolNames.WindowLevel,
|
||||
toolGroupId: toolGroupIds.CT,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: toolNames.WindowLevel,
|
||||
toolGroupId: toolGroupIds.PT,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: toolNames.WindowLevel,
|
||||
toolGroupId: toolGroupIds.Fusion,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const { unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
() => {
|
||||
@ -110,13 +77,10 @@ function modeFactory({ modeConfiguration }) {
|
||||
toolGroupService,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
setWindowLevelActive();
|
||||
}
|
||||
);
|
||||
|
||||
unsubscriptions.push(unsubscribe);
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
@ -124,6 +88,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
'WindowLevel',
|
||||
'Crosshairs',
|
||||
'Pan',
|
||||
'SyncToggle',
|
||||
'RectangleROIStartEndThreshold',
|
||||
'fusionPTColormap',
|
||||
]);
|
||||
|
||||
@ -60,6 +60,7 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) {
|
||||
toolName: toolNames.Crosshairs,
|
||||
configuration: {
|
||||
viewportIndicators: false,
|
||||
disableOnPassive: true,
|
||||
autoPan: {
|
||||
enabled: false,
|
||||
panSize: 10,
|
||||
|
||||
@ -1,26 +1,8 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
// Only ways that you can pass in a custom React component for render :l
|
||||
import { defaults, ToolbarService } from '@ohif/core';
|
||||
import { WindowLevelMenuItem } from '@ohif/ui';
|
||||
import { defaults } from '@ohif/core';
|
||||
import { toolGroupIds } from './initToolGroups';
|
||||
|
||||
const { windowLevelPresets } = defaults;
|
||||
/**
|
||||
*
|
||||
* @param {*} type - 'tool' | 'action' | 'toggle'
|
||||
* @param {*} id
|
||||
* @param {*} icon
|
||||
* @param {*} label
|
||||
*/
|
||||
function _createButton(type, id, icon, label, commands, tooltip) {
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
type,
|
||||
commands,
|
||||
tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
function _createColormap(label, colormap) {
|
||||
return {
|
||||
@ -38,23 +20,11 @@ function _createColormap(label, colormap) {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const _createActionButton = _createButton.bind(null, 'action');
|
||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} preset - preset number (from above import)
|
||||
* @param {*} title
|
||||
* @param {*} subtitle
|
||||
*/
|
||||
function _createWwwcPreset(preset, title, subtitle) {
|
||||
return {
|
||||
id: preset.toString(),
|
||||
title,
|
||||
subtitle,
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setWindowLevel',
|
||||
@ -67,170 +37,103 @@ function _createWwwcPreset(preset, title, subtitle) {
|
||||
};
|
||||
}
|
||||
|
||||
function _createCommands(commandName, toolName, toolGroupIds) {
|
||||
return toolGroupIds.map(toolGroupId => ({
|
||||
/* It's a command that is being run when the button is clicked. */
|
||||
commandName,
|
||||
commandOptions: {
|
||||
toolName,
|
||||
toolGroupId,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
}));
|
||||
}
|
||||
const setToolActiveToolbar = {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
toolGroupIds: [toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion],
|
||||
},
|
||||
};
|
||||
|
||||
const toolbarButtons = [
|
||||
// Measurement
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
isRadio: true, // ?
|
||||
// Switch?
|
||||
primary: _createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
..._createCommands('setToolActive', 'Length', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
'Length'
|
||||
),
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
tooltip: 'Length Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: '',
|
||||
isActive: true,
|
||||
tooltip: 'More Measure Tools',
|
||||
},
|
||||
items: [
|
||||
_createToolButton(
|
||||
'Length',
|
||||
'tool-length',
|
||||
'Length',
|
||||
[
|
||||
..._createCommands('setToolActive', 'Length', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
'Length Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'Bidirectional',
|
||||
'tool-bidirectional',
|
||||
'Bidirectional',
|
||||
[
|
||||
..._createCommands('setToolActive', 'Bidirectional', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
'Bidirectional Tool'
|
||||
),
|
||||
_createToolButton(
|
||||
'ArrowAnnotate',
|
||||
'tool-annotate',
|
||||
'Annotation',
|
||||
[
|
||||
..._createCommands('setToolActive', 'ArrowAnnotate', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
'Arrow Annotate'
|
||||
),
|
||||
_createToolButton(
|
||||
'EllipticalROI',
|
||||
'tool-elipse',
|
||||
'Ellipse',
|
||||
[
|
||||
..._createCommands('setToolActive', 'EllipticalROI', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
'Ellipse Tool'
|
||||
),
|
||||
ToolbarService.createButton({
|
||||
id: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
label: 'Bidirectional',
|
||||
tooltip: 'Bidirectional Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'ArrowAnnotate',
|
||||
icon: 'tool-annotate',
|
||||
label: 'Arrow Annotate',
|
||||
tooltip: 'Arrow Annotate Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
ToolbarService.createButton({
|
||||
id: 'EllipticalROI',
|
||||
icon: 'tool-ellipse',
|
||||
label: 'Ellipse',
|
||||
tooltip: 'Ellipse Tool',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Zoom..
|
||||
{
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: [
|
||||
..._createCommands('setToolActive', 'Zoom', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MPR',
|
||||
type: 'ohif.action',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'toggle',
|
||||
icon: 'icon-mpr',
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
commandOptions: { protocolId: 'mpr' },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
evaluate: 'evaluate.mpr',
|
||||
},
|
||||
},
|
||||
// Window Level + Presets...
|
||||
// Window Level + Presets
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: _createToolButton(
|
||||
'WindowLevel',
|
||||
'tool-window-level',
|
||||
'Window Level',
|
||||
[
|
||||
..._createCommands('setToolActive', 'WindowLevel', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
'Window Level'
|
||||
),
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'WindowLevel',
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
tooltip: 'Window Level',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: 'W/L Manual',
|
||||
isActive: true,
|
||||
tooltip: 'W/L Presets',
|
||||
},
|
||||
isAction: true, // ?
|
||||
renderer: WindowLevelMenuItem,
|
||||
items: [
|
||||
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
|
||||
@ -241,86 +144,57 @@ const toolbarButtons = [
|
||||
],
|
||||
},
|
||||
},
|
||||
// Crosshairs Button
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: [
|
||||
..._createCommands('setToolActive', 'Crosshairs', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Pan...
|
||||
// Pan Button
|
||||
{
|
||||
id: 'Pan',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: [
|
||||
..._createCommands('setToolActive', 'Pan', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
// toolGroupIds.MPR,
|
||||
]),
|
||||
],
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Rectangle ROI Start End Threshold Button
|
||||
{
|
||||
id: 'RectangleROIStartEndThreshold',
|
||||
type: 'ohif.radioGroup',
|
||||
uiType: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-create-threshold',
|
||||
label: 'Rectangle ROI Threshold',
|
||||
commands: [
|
||||
..._createCommands('setToolActive', 'RectangleROIStartEndThreshold', [toolGroupIds.PT]),
|
||||
{
|
||||
commandName: 'displayNotification',
|
||||
commandOptions: {
|
||||
title: 'RectangleROI Threshold Tip',
|
||||
text: 'RectangleROI Threshold tool should be used on PT Axial Viewport',
|
||||
type: 'info',
|
||||
},
|
||||
},
|
||||
{
|
||||
commandName: 'setViewportActive',
|
||||
commandOptions: {
|
||||
viewportId: 'ptAXIAL',
|
||||
},
|
||||
},
|
||||
],
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
},
|
||||
},
|
||||
// Fusion PT Colormap Button
|
||||
{
|
||||
id: 'fusionPTColormap',
|
||||
type: 'ohif.splitButton',
|
||||
uiType: 'ohif.splitButton',
|
||||
props: {
|
||||
groupId: 'fusionPTColormap',
|
||||
primary: _createToolButton(
|
||||
'fusionPTColormap',
|
||||
'tool-fusion-color',
|
||||
'Fusion PT Colormap',
|
||||
[],
|
||||
'Fusion PT Colormap'
|
||||
),
|
||||
primary: ToolbarService.createButton({
|
||||
id: 'fusionPTColormap',
|
||||
icon: 'tool-fusion-color',
|
||||
label: 'Fusion PT Colormap',
|
||||
tooltip: 'Fusion PT Colormap',
|
||||
commands: [],
|
||||
evaluate: 'evaluate.action',
|
||||
}),
|
||||
secondary: {
|
||||
icon: 'chevron-down',
|
||||
label: 'PT Colormap',
|
||||
isActive: true,
|
||||
tooltip: 'PET Image Colormap',
|
||||
},
|
||||
isAction: true, // ?
|
||||
items: [
|
||||
_createColormap('HSV', 'hsv'),
|
||||
_createColormap('Hot Iron', 'hot_iron'),
|
||||
|
||||
@ -72,7 +72,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
// Assign an alias to the button element
|
||||
cy.get('@wwwcBtnPrimary').as('wwwcButton');
|
||||
cy.get('@wwwcButton').click();
|
||||
cy.get('@wwwcButton').should('have.class', 'active');
|
||||
cy.get('@wwwcButton').should('have.class', 'bg-primary-light');
|
||||
|
||||
//drags the mouse inside the viewport to be able to interact with series
|
||||
cy.get('@viewport')
|
||||
@ -97,7 +97,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('@panButton').click();
|
||||
|
||||
// Assert that the button has the 'active' class
|
||||
cy.get('@panButton').should('have.class', 'active');
|
||||
cy.get('@panButton').should('have.class', 'bg-primary-light');
|
||||
|
||||
// Trigger the pan actions on the viewport
|
||||
cy.get('@viewport')
|
||||
@ -418,44 +418,44 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('@moreBtnSecondary').click();
|
||||
|
||||
//Click on Flip button
|
||||
cy.get('[data-cy="flip-horizontal"]').click();
|
||||
cy.get('[data-cy="flipHorizontal"]').click();
|
||||
cy.waitDicomImage();
|
||||
cy.get('@viewportInfoMidLeft').should('contains.text', 'L');
|
||||
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
|
||||
});
|
||||
|
||||
it('checks if stack sync is preserved on new display set and uses FOR', () => {
|
||||
// Active stack image sync and reference lines
|
||||
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
|
||||
cy.get('[data-cy="ImageSliceSync"]').click();
|
||||
// Add reference lines as that sometimes throws an exception
|
||||
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
|
||||
cy.get('[data-cy="ReferenceLines"]').click();
|
||||
// it('checks if stack sync is preserved on new display set and uses FOR', () => {
|
||||
// // Active stack image sync and reference lines
|
||||
// cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
|
||||
// cy.get('[data-cy="ImageSliceSync"]').click();
|
||||
// // Add reference lines as that sometimes throws an exception
|
||||
// cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
|
||||
// cy.get('[data-cy="ReferenceLines"]').click();
|
||||
|
||||
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
|
||||
cy.get('body').type('{downarrow}{downarrow}');
|
||||
// cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
|
||||
// cy.get('body').type('{downarrow}{downarrow}');
|
||||
|
||||
// Change the layout and double load the first
|
||||
cy.setLayout(2, 1);
|
||||
cy.get('body').type('{rightarrow}');
|
||||
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
|
||||
cy.waitDicomImage();
|
||||
// // Change the layout and double load the first
|
||||
// cy.setLayout(2, 1);
|
||||
// cy.get('body').type('{rightarrow}');
|
||||
// cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
|
||||
// cy.waitDicomImage();
|
||||
|
||||
// Now navigate down once and check that the left hand pane navigated
|
||||
cy.get('body').focus().type('{downarrow}');
|
||||
// // Now navigate down once and check that the left hand pane navigated
|
||||
// cy.get('body').focus().type('{downarrow}');
|
||||
|
||||
// The following lines assist in troubleshooting when/if this test were to fail.
|
||||
cy.get('[data-cy="viewport-pane"]')
|
||||
.eq(0)
|
||||
.find('[data-cy="viewport-overlay-top-right"]')
|
||||
.should('contains.text', 'I:2 (2/20)');
|
||||
cy.get('[data-cy="viewport-pane"]')
|
||||
.eq(1)
|
||||
.find('[data-cy="viewport-overlay-top-right"]')
|
||||
.should('contains.text', 'I:2 (2/20)');
|
||||
// // The following lines assist in troubleshooting when/if this test were to fail.
|
||||
// cy.get('[data-cy="viewport-pane"]')
|
||||
// .eq(0)
|
||||
// .find('[data-cy="viewport-overlay-top-right"]')
|
||||
// .should('contains.text', 'I:2 (2/20)');
|
||||
// cy.get('[data-cy="viewport-pane"]')
|
||||
// .eq(1)
|
||||
// .find('[data-cy="viewport-overlay-top-right"]')
|
||||
// .should('contains.text', 'I:2 (2/20)');
|
||||
|
||||
cy.get('body').type('{leftarrow}');
|
||||
cy.setLayout(1, 1);
|
||||
cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/20)');
|
||||
});
|
||||
// cy.get('body').type('{leftarrow}');
|
||||
// cy.setLayout(1, 1);
|
||||
// cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/20)');
|
||||
// });
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ describe('OHIF General Viewer', function () {
|
||||
cy.get('@zoomBtn')
|
||||
.click()
|
||||
.then($zoomBtn => {
|
||||
cy.wrap($zoomBtn).should('have.class', 'active');
|
||||
cy.wrap($zoomBtn).should('have.class', 'bg-primary-light');
|
||||
});
|
||||
|
||||
const zoomLevelInitial = cy.get('@viewportInfoTopLeft').then($viewportInfo => {
|
||||
|
||||
@ -7,8 +7,7 @@ describe('OHIF MPR', () => {
|
||||
});
|
||||
|
||||
it('should not go MPR for non reconstructible displaySets', () => {
|
||||
cy.get('[data-cy="MPR"]').click();
|
||||
cy.get('.cornerstone-canvas').should('have.length', 1);
|
||||
cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled');
|
||||
});
|
||||
|
||||
it('should go MPR for reconstructible displaySets and come back', () => {
|
||||
@ -88,7 +87,6 @@ describe('OHIF MPR', () => {
|
||||
});
|
||||
|
||||
it('should correctly render Crosshairs for MPR', () => {
|
||||
cy.get('[data-cy="Crosshairs"]').should('not.exist');
|
||||
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
|
||||
cy.get('[data-cy="MPR"]').click();
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
@ -125,13 +123,7 @@ describe('OHIF MPR', () => {
|
||||
cy.get('[data-cy="MPR"]').click();
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
|
||||
// wait for the crosshairs tool to be active
|
||||
cy.get('[data-cy="Crosshairs"].active');
|
||||
|
||||
// Click the crosshairs button to deactivate it.
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
|
||||
// wait for the window level button to be active
|
||||
cy.get('[data-cy="WindowLevel-split-button-primary"].active');
|
||||
});
|
||||
});
|
||||
|
||||
@ -273,7 +273,7 @@ Cypress.Commands.add(
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('@lengthButton').should('have.class', 'active');
|
||||
cy.get('@lengthButton').should('have.class', 'bg-primary-light');
|
||||
|
||||
cy.get('@viewport').then($viewport => {
|
||||
const [x1, y1] = firstClick;
|
||||
|
||||
@ -10,6 +10,9 @@ window.config = {
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
// filterQueryParam: false,
|
||||
defaultDataSourceName: 'e2e',
|
||||
investigationalUseDialog: {
|
||||
option: 'never',
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
ViewportGridProvider,
|
||||
CineProvider,
|
||||
UserAuthenticationProvider,
|
||||
ToolboxProvider,
|
||||
} from '@ohif/ui';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
@ -69,6 +70,7 @@ function App({ config, defaultExtensions, defaultModes }) {
|
||||
[UserAuthenticationProvider, { service: userAuthenticationService }],
|
||||
[I18nextProvider, { i18n }],
|
||||
[ThemeWrapper],
|
||||
[ToolboxProvider],
|
||||
[ViewportGridProvider, { service: viewportGridService }],
|
||||
[ViewportDialogProvider, { service: uiViewportDialogService }],
|
||||
[CineProvider, { service: cineService }],
|
||||
|
||||
@ -49,6 +49,8 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
appConfig,
|
||||
});
|
||||
|
||||
servicesManager.setExtensionManager(extensionManager);
|
||||
|
||||
servicesManager.registerServices([
|
||||
UINotificationService.REGISTRATION,
|
||||
UIModalService.REGISTRATION,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useEffect, useCallback, useRef, useState } from 'react';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ServicesManager, Types, MeasurementService } from '@ohif/core';
|
||||
@ -15,12 +15,15 @@ function ViewerViewportGrid(props) {
|
||||
const { layout, activeViewportId, viewports } = viewportGrid;
|
||||
const { numCols, numRows } = layout;
|
||||
const elementRef = useRef(null);
|
||||
const layoutHash = useRef(null);
|
||||
|
||||
// TODO -> Need some way of selecting which displaySets hit the viewports.
|
||||
const { displaySetService, measurementService, hangingProtocolService, uiNotificationService } = (
|
||||
servicesManager as ServicesManager
|
||||
).services;
|
||||
|
||||
const generateLayoutHash = () => `${numCols}-${numRows}`;
|
||||
|
||||
/**
|
||||
* This callback runs after the viewports structure has changed in any way.
|
||||
* On initial display, that means if it has changed by applying a HangingProtocol,
|
||||
@ -135,6 +138,16 @@ function ViewerViewportGrid(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check viewport readiness in useEffect
|
||||
useEffect(() => {
|
||||
const allReady = viewportGridService.getGridViewportsReady();
|
||||
const sameLayoutHash = layoutHash.current === generateLayoutHash();
|
||||
if (allReady && !sameLayoutHash) {
|
||||
layoutHash.current = generateLayoutHash();
|
||||
viewportGridService.publishViewportsReady();
|
||||
}
|
||||
}, [viewportGridService, generateLayoutHash]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
|
||||
@ -327,6 +340,9 @@ function ViewerViewportGrid(props) {
|
||||
viewportOptions={viewportOptions}
|
||||
displaySetOptions={displaySetOptions}
|
||||
needsRerendering={displaySetsNeedsRerendering}
|
||||
onReady={() => {
|
||||
viewportGridService.setViewportIsReady(viewportId, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ViewportPane>
|
||||
@ -360,7 +376,6 @@ function ViewerViewportGrid(props) {
|
||||
}}
|
||||
targetRef={elementRef.current}
|
||||
/>
|
||||
{/* {ViewportPanes} */}
|
||||
{getViewportPanes()}
|
||||
</ViewportGrid>
|
||||
</div>
|
||||
|
||||
@ -1,141 +1,18 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef, useContext, createContext } from 'react';
|
||||
import { useParams, useLocation, useNavigate } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
// TODO: DicomMetadataStore should be injected?
|
||||
import { DicomMetadataStore, ServicesManager, utils, log, Enums } from '@ohif/core';
|
||||
import { ServicesManager, utils } from '@ohif/core';
|
||||
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
|
||||
import { useSearchParams } from '@hooks';
|
||||
import { useAppConfig } from '@state';
|
||||
import ViewportGrid from '@components/ViewportGrid';
|
||||
import Compose from './Compose';
|
||||
import getStudies from './studiesList';
|
||||
import { history } from '../../utils/history';
|
||||
import loadModules from '../../pluginImports';
|
||||
import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed';
|
||||
import { defaultRouteInit } from './defaultRouteInit';
|
||||
import { updateAuthServiceAndCleanUrl } from './updateAuthServiceAndCleanUrl';
|
||||
|
||||
const { getSplitParam, sortingCriteria } = utils;
|
||||
|
||||
/**
|
||||
* Initialize the route.
|
||||
*
|
||||
* @param props.servicesManager to read services from
|
||||
* @param props.studyInstanceUIDs for a list of studies to read
|
||||
* @param props.dataSource to read the data from
|
||||
* @param props.filters filters from query params to read the data from
|
||||
* @returns array of subscriptions to cancel
|
||||
*/
|
||||
function defaultRouteInit(
|
||||
{ servicesManager, studyInstanceUIDs, dataSource, filters, appConfig },
|
||||
hangingProtocolId
|
||||
) {
|
||||
const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
/**
|
||||
* Function to apply the hanging protocol when the minimum number of display sets were
|
||||
* received or all display sets retrieval were completed
|
||||
* @returns
|
||||
*/
|
||||
function applyHangingProtocol() {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
if (!displaySets || !displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Gets the studies list to use
|
||||
const studies = getStudies(studyInstanceUIDs, displaySets);
|
||||
|
||||
// study being displayed, and is thus the "active" study.
|
||||
const activeStudy = studies[0];
|
||||
|
||||
// run the hanging protocol matching on the displaySets with the predefined
|
||||
// hanging protocol in the mode configuration
|
||||
hangingProtocolService.run({ studies, activeStudy, displaySets }, hangingProtocolId);
|
||||
}
|
||||
|
||||
const unsubscriptions = [];
|
||||
const issuedWarningSeries = [];
|
||||
const { unsubscribe: instanceAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
|
||||
// checks if the series filter was used, if it exists
|
||||
const seriesInstanceUIDs = filters?.seriesInstanceUID;
|
||||
if (
|
||||
seriesInstanceUIDs?.length &&
|
||||
!isSeriesFilterUsed(seriesMetadata.instances, filters) &&
|
||||
!issuedWarningSeries.includes(seriesInstanceUIDs[0])
|
||||
) {
|
||||
// stores the series instance filter so it shows only once the warning
|
||||
issuedWarningSeries.push(seriesInstanceUIDs[0]);
|
||||
uiNotificationService.show({
|
||||
title: 'Series filter',
|
||||
message: `Each of the series in filter: ${seriesInstanceUIDs} are not part of the current study. The entire study is being displayed`,
|
||||
type: 'error',
|
||||
duration: 7000,
|
||||
});
|
||||
}
|
||||
|
||||
displaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
|
||||
}
|
||||
);
|
||||
|
||||
unsubscriptions.push(instanceAddedUnsubscribe);
|
||||
|
||||
log.time(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
|
||||
log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
|
||||
|
||||
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
|
||||
dataSource.retrieve.series.metadata({
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
returnPromises: true,
|
||||
sortCriteria:
|
||||
customizationService.get('sortingCriteria') ||
|
||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||
})
|
||||
);
|
||||
|
||||
// log the error if this fails, otherwise it's so difficult to tell what went wrong...
|
||||
allRetrieves.forEach(retrieve => {
|
||||
retrieve.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.allSettled(allRetrieves).then(promises => {
|
||||
log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
|
||||
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
|
||||
const allPromises = [];
|
||||
const remainingPromises = [];
|
||||
|
||||
function startRemainingPromises(remainingPromises) {
|
||||
remainingPromises.forEach(p => p.forEach(p => p.start()));
|
||||
}
|
||||
|
||||
promises.forEach(promise => {
|
||||
const retrieveSeriesMetadataPromise = promise.value;
|
||||
if (Array.isArray(retrieveSeriesMetadataPromise)) {
|
||||
const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun(
|
||||
hangingProtocolId,
|
||||
retrieveSeriesMetadataPromise
|
||||
);
|
||||
const requiredSeriesPromises = requiredSeries.map(promise => promise.start());
|
||||
allPromises.push(Promise.allSettled(requiredSeriesPromises));
|
||||
remainingPromises.push(remaining);
|
||||
}
|
||||
});
|
||||
|
||||
Promise.allSettled(allPromises).then(applyHangingProtocol);
|
||||
startRemainingPromises(remainingPromises);
|
||||
applyHangingProtocol();
|
||||
});
|
||||
|
||||
return unsubscriptions;
|
||||
}
|
||||
const { getSplitParam } = utils;
|
||||
|
||||
export default function ModeRoute({
|
||||
mode,
|
||||
@ -166,7 +43,7 @@ export default function ModeRoute({
|
||||
// The URL's query search parameters where the keys are all lower case.
|
||||
const lowerCaseSearchParams = useSearchParams({ lowerCaseKeys: true });
|
||||
|
||||
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState();
|
||||
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState(null);
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [ExtensionDependenciesLoaded, setExtensionDependenciesLoaded] = useState(false);
|
||||
@ -193,25 +70,7 @@ export default function ModeRoute({
|
||||
const token = lowerCaseSearchParams.get('token');
|
||||
|
||||
if (token) {
|
||||
// if a token is passed in, set the userAuthenticationService to use it
|
||||
// for the Authorization header for all requests
|
||||
userAuthenticationService.setServiceImplementation({
|
||||
getAuthorizationHeader: () => ({
|
||||
Authorization: 'Bearer ' + token,
|
||||
}),
|
||||
});
|
||||
|
||||
// Create a URL object with the current location
|
||||
const urlObj = new URL(window.location.origin + window.location.pathname + location.search);
|
||||
|
||||
// Remove the token from the URL object
|
||||
urlObj.searchParams.delete('token');
|
||||
const cleanUrl = urlObj.toString();
|
||||
|
||||
// Update the browser's history without the token
|
||||
if (window.history && window.history.replaceState) {
|
||||
window.history.replaceState(null, '', cleanUrl);
|
||||
}
|
||||
updateAuthServiceAndCleanUrl(token, location, userAuthenticationService);
|
||||
}
|
||||
|
||||
// Preserve the old array interface for hotkeys
|
||||
@ -228,32 +87,6 @@ export default function ModeRoute({
|
||||
// Only handling one route per mode for now
|
||||
const route = mode.routes[0];
|
||||
|
||||
// For each extension, look up their context modules
|
||||
// TODO: move to extension manager.
|
||||
let contextModules = [];
|
||||
|
||||
Object.keys(extensions).forEach(extensionId => {
|
||||
const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap);
|
||||
const moduleIds = allRegisteredModuleIds.filter(id =>
|
||||
id.includes(`${extensionId}.contextModule.`)
|
||||
);
|
||||
|
||||
if (!moduleIds || !moduleIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modules = moduleIds.map(extensionManager.getModuleEntry);
|
||||
contextModules = contextModules.concat(modules);
|
||||
});
|
||||
|
||||
const contextModuleProviders = contextModules.map(a => a.provider);
|
||||
const CombinedContextProvider = ({ children }) =>
|
||||
Compose({ components: contextModuleProviders, children });
|
||||
|
||||
function ViewportGridWithDataSource(props) {
|
||||
return ViewportGrid({ ...props, dataSource });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadExtensions = async () => {
|
||||
const loadedExtensions = await loadModules(Object.keys(extensions));
|
||||
@ -298,7 +131,7 @@ export default function ModeRoute({
|
||||
}, [location, ExtensionDependenciesLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ExtensionDependenciesLoaded) {
|
||||
if (!ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -313,7 +146,7 @@ export default function ModeRoute({
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
};
|
||||
if (studyInstanceUIDs?.length && studyInstanceUIDs[0] !== undefined) {
|
||||
if (Array.isArray(studyInstanceUIDs) && studyInstanceUIDs[0]) {
|
||||
retrieveLayoutData();
|
||||
}
|
||||
return () => {
|
||||
@ -322,7 +155,7 @@ export default function ModeRoute({
|
||||
}, [studyInstanceUIDs, ExtensionDependenciesLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hotkeys || !ExtensionDependenciesLoaded) {
|
||||
if (!hotkeys || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -339,10 +172,10 @@ export default function ModeRoute({
|
||||
return () => {
|
||||
hotkeysManager.destroy();
|
||||
};
|
||||
}, [ExtensionDependenciesLoaded]);
|
||||
}, [ExtensionDependenciesLoaded, hotkeys, studyInstanceUIDs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layoutTemplateData.current || !ExtensionDependenciesLoaded) {
|
||||
if (!layoutTemplateData.current || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -477,7 +310,21 @@ export default function ModeRoute({
|
||||
refresh,
|
||||
]);
|
||||
|
||||
const renderLayoutData = props => {
|
||||
if (!studyInstanceUIDs || !layoutTemplateData.current || !ExtensionDependenciesLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ViewportGridWithDataSource = props => {
|
||||
return ViewportGrid({ ...props, dataSource });
|
||||
};
|
||||
|
||||
const CombinedExtensionsContextProvider = createCombinedContextProvider(
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
commandsManager
|
||||
);
|
||||
|
||||
const getLayoutComponent = props => {
|
||||
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
|
||||
layoutTemplateData.current.id
|
||||
);
|
||||
@ -486,27 +333,51 @@ export default function ModeRoute({
|
||||
return <LayoutComponent {...props} />;
|
||||
};
|
||||
|
||||
const LayoutComponent = getLayoutComponent({
|
||||
...layoutTemplateData.current.props,
|
||||
ViewportGridComp: ViewportGridWithDataSource,
|
||||
});
|
||||
|
||||
return (
|
||||
<ImageViewerProvider
|
||||
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
|
||||
StudyInstanceUIDs={studyInstanceUIDs}
|
||||
// reducer={reducer}
|
||||
>
|
||||
<CombinedContextProvider>
|
||||
<DragAndDropProvider>
|
||||
{layoutTemplateData.current &&
|
||||
studyInstanceUIDs?.[0] !== undefined &&
|
||||
ExtensionDependenciesLoaded &&
|
||||
renderLayoutData({
|
||||
...layoutTemplateData.current.props,
|
||||
ViewportGridComp: ViewportGridWithDataSource,
|
||||
})}
|
||||
</DragAndDropProvider>
|
||||
</CombinedContextProvider>
|
||||
<ImageViewerProvider StudyInstanceUIDs={studyInstanceUIDs}>
|
||||
{CombinedExtensionsContextProvider ? (
|
||||
<CombinedExtensionsContextProvider>
|
||||
<DragAndDropProvider>{LayoutComponent}</DragAndDropProvider>
|
||||
</CombinedExtensionsContextProvider>
|
||||
) : (
|
||||
<DragAndDropProvider>{LayoutComponent}</DragAndDropProvider>
|
||||
)}
|
||||
</ImageViewerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a combined context provider using the context modules from the extension manager.
|
||||
* @param {object} extensionManager - The extension manager instance.
|
||||
* @param {object} servicesManager - The services manager instance.
|
||||
* @param {object} commandsManager - The commands manager instance.
|
||||
* @returns {React.Component} - A React component that provides combined contexts to its children.
|
||||
*/
|
||||
function createCombinedContextProvider(extensionManager, servicesManager, commandsManager) {
|
||||
const extensionsContextModules = extensionManager.getModulesByType(
|
||||
extensionManager.constructor.MODULE_TYPES.CONTEXT
|
||||
);
|
||||
|
||||
if (!extensionsContextModules?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contextModuleProviders = extensionsContextModules.flatMap(({ module }) => {
|
||||
return module.map(aContextModule => {
|
||||
return aContextModule.provider;
|
||||
});
|
||||
});
|
||||
|
||||
return ({ children }) => {
|
||||
return Compose({ components: contextModuleProviders, children });
|
||||
};
|
||||
}
|
||||
|
||||
ModeRoute.propTypes = {
|
||||
mode: PropTypes.object.isRequired,
|
||||
dataSourceName: PropTypes.string,
|
||||
|
||||
129
platform/app/src/routes/Mode/defaultRouteInit.ts
Normal file
129
platform/app/src/routes/Mode/defaultRouteInit.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import getStudies from './studiesList';
|
||||
import { DicomMetadataStore, log } from '@ohif/core';
|
||||
import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed';
|
||||
|
||||
import { utils, Enums } from '@ohif/core';
|
||||
|
||||
const { sortingCriteria } = utils;
|
||||
|
||||
/**
|
||||
* Initialize the route.
|
||||
*
|
||||
* @param props.servicesManager to read services from
|
||||
* @param props.studyInstanceUIDs for a list of studies to read
|
||||
* @param props.dataSource to read the data from
|
||||
* @param props.filters filters from query params to read the data from
|
||||
* @returns array of subscriptions to cancel
|
||||
*/
|
||||
export function defaultRouteInit(
|
||||
{ servicesManager, studyInstanceUIDs, dataSource, filters, appConfig },
|
||||
hangingProtocolId
|
||||
) {
|
||||
const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
/**
|
||||
* Function to apply the hanging protocol when the minimum number of display sets were
|
||||
* received or all display sets retrieval were completed
|
||||
* @returns
|
||||
*/
|
||||
function applyHangingProtocol() {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
if (!displaySets || !displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Gets the studies list to use
|
||||
const studies = getStudies(studyInstanceUIDs, displaySets);
|
||||
|
||||
// study being displayed, and is thus the "active" study.
|
||||
const activeStudy = studies[0];
|
||||
|
||||
// run the hanging protocol matching on the displaySets with the predefined
|
||||
// hanging protocol in the mode configuration
|
||||
hangingProtocolService.run({ studies, activeStudy, displaySets }, hangingProtocolId);
|
||||
}
|
||||
|
||||
const unsubscriptions = [];
|
||||
const issuedWarningSeries = [];
|
||||
const { unsubscribe: instanceAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
|
||||
// checks if the series filter was used, if it exists
|
||||
const seriesInstanceUIDs = filters?.seriesInstanceUID;
|
||||
if (
|
||||
seriesInstanceUIDs?.length &&
|
||||
!isSeriesFilterUsed(seriesMetadata.instances, filters) &&
|
||||
!issuedWarningSeries.includes(seriesInstanceUIDs[0])
|
||||
) {
|
||||
// stores the series instance filter so it shows only once the warning
|
||||
issuedWarningSeries.push(seriesInstanceUIDs[0]);
|
||||
uiNotificationService.show({
|
||||
title: 'Series filter',
|
||||
message: `Each of the series in filter: ${seriesInstanceUIDs} are not part of the current study. The entire study is being displayed`,
|
||||
type: 'error',
|
||||
duration: 7000,
|
||||
});
|
||||
}
|
||||
|
||||
displaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
|
||||
}
|
||||
);
|
||||
|
||||
unsubscriptions.push(instanceAddedUnsubscribe);
|
||||
|
||||
log.time(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
|
||||
log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
|
||||
|
||||
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
|
||||
dataSource.retrieve.series.metadata({
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
returnPromises: true,
|
||||
sortCriteria:
|
||||
customizationService.get('sortingCriteria') ||
|
||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||
})
|
||||
);
|
||||
|
||||
// log the error if this fails, otherwise it's so difficult to tell what went wrong...
|
||||
allRetrieves.forEach(retrieve => {
|
||||
retrieve.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.allSettled(allRetrieves).then(promises => {
|
||||
log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
|
||||
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
|
||||
const allPromises = [];
|
||||
const remainingPromises = [];
|
||||
|
||||
function startRemainingPromises(remainingPromises) {
|
||||
remainingPromises.forEach(p => p.forEach(p => p.start()));
|
||||
}
|
||||
|
||||
promises.forEach(promise => {
|
||||
const retrieveSeriesMetadataPromise = promise.value;
|
||||
if (Array.isArray(retrieveSeriesMetadataPromise)) {
|
||||
const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun(
|
||||
hangingProtocolId,
|
||||
retrieveSeriesMetadataPromise
|
||||
);
|
||||
const requiredSeriesPromises = requiredSeries.map(promise => promise.start());
|
||||
allPromises.push(Promise.allSettled(requiredSeriesPromises));
|
||||
remainingPromises.push(remaining);
|
||||
}
|
||||
});
|
||||
|
||||
Promise.allSettled(allPromises).then(applyHangingProtocol);
|
||||
startRemainingPromises(remainingPromises);
|
||||
applyHangingProtocol();
|
||||
});
|
||||
|
||||
return unsubscriptions;
|
||||
}
|
||||
35
platform/app/src/routes/Mode/updateAuthServiceAndCleanUrl.ts
Normal file
35
platform/app/src/routes/Mode/updateAuthServiceAndCleanUrl.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Updates the user authentication service with the provided token and cleans the token from the URL.
|
||||
* @param token - The token to set in the user authentication service.
|
||||
* @param location - The location object from the router.
|
||||
* @param userAuthenticationService - The user authentication service instance.
|
||||
*/
|
||||
export function updateAuthServiceAndCleanUrl(
|
||||
token: string,
|
||||
location: any,
|
||||
userAuthenticationService: any
|
||||
): void {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if a token is passed in, set the userAuthenticationService to use it
|
||||
// for the Authorization header for all requests
|
||||
userAuthenticationService.setServiceImplementation({
|
||||
getAuthorizationHeader: () => ({
|
||||
Authorization: 'Bearer ' + token,
|
||||
}),
|
||||
});
|
||||
|
||||
// Create a URL object with the current location
|
||||
const urlObj = new URL(window.location.origin + window.location.pathname + location.search);
|
||||
|
||||
// Remove the token from the URL object
|
||||
urlObj.searchParams.delete('token');
|
||||
const cleanUrl = urlObj.toString();
|
||||
|
||||
// Update the browser's history without the token
|
||||
if (window.history && window.history.replaceState) {
|
||||
window.history.replaceState(null, '', cleanUrl);
|
||||
}
|
||||
}
|
||||
@ -419,7 +419,7 @@ function WorkList({
|
||||
/>
|
||||
} // launch-arrow | launch-info
|
||||
onClick={() => {}}
|
||||
data-cy={`mode-${mode.routeName}-${studyInstanceUid}`}
|
||||
dataCY={`mode-${mode.routeName}-${studyInstanceUid}`}
|
||||
className={isValidMode ? 'text-[13px]' : 'bg-[#222d44] text-[13px]'}
|
||||
>
|
||||
{mode.displayName}
|
||||
|
||||
@ -48,37 +48,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||
|
||||
let unsubscribe;
|
||||
|
||||
const activateTool = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// We don't need to reset the active tool whenever a viewport is getting
|
||||
// added to the toolGroup.
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
// Since we only have one viewport for the basic cs3d mode and it has
|
||||
// only one hanging protocol, we can just use the first viewport
|
||||
({ unsubscribe } = toolGroupService.subscribe(
|
||||
toolGroupService.EVENTS.VIEWPORT_ADDED,
|
||||
activateTool
|
||||
));
|
||||
|
||||
toolbarService.init(extensionManager);
|
||||
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
|
||||
toolbarService.addButtons(toolbarButtons);
|
||||
toolbarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
|
||||
@ -8,7 +8,6 @@ describe('CommandsManager', () => {
|
||||
contextName = 'VTK',
|
||||
command = {
|
||||
commandFn: jest.fn().mockReturnValue(true),
|
||||
storeContexts: ['viewers'],
|
||||
options: { passMeToCommandFn: ':wave:' },
|
||||
},
|
||||
commandsManagerConfig = {
|
||||
@ -139,7 +138,6 @@ describe('CommandsManager', () => {
|
||||
it('Logs a warning if command definition does not have a commandFn', () => {
|
||||
const commandWithNoCommmandFn = {
|
||||
commandFn: undefined,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import log from '../log.js';
|
||||
import { Command, Commands } from '../types/Command';
|
||||
import { Command, Commands, ComplexCommand } from '../types/Command';
|
||||
|
||||
/**
|
||||
* The definition of a command
|
||||
@ -167,28 +167,44 @@ export class CommandsManager {
|
||||
* Run one or more commands with specified extra options.
|
||||
* Returns the result of the last command run.
|
||||
*
|
||||
* @param toRun - A specification of one or more commands
|
||||
* @param toRun - A specification of one or more commands,
|
||||
* typically an object of { commandName, commandOptions, context }
|
||||
* or an array of such objects. It can also be a single commandName as string
|
||||
* if no options are needed.
|
||||
* @param options - to include in the commands run beyond
|
||||
* the commandOptions specified in the base.
|
||||
*/
|
||||
public run(
|
||||
toRun: Command | Commands | Command[] | undefined,
|
||||
toRun: Command | Commands | Command[] | string | undefined,
|
||||
options?: Record<string, unknown>
|
||||
): unknown {
|
||||
if (!toRun) {
|
||||
return;
|
||||
}
|
||||
const commands =
|
||||
(Array.isArray(toRun) && toRun) ||
|
||||
((toRun as Command).commandName && [toRun]) ||
|
||||
(Array.isArray((toRun as Commands).commands) && (toRun as Commands).commands);
|
||||
if (!commands) {
|
||||
|
||||
// Normalize `toRun` to an array of `ComplexCommand`
|
||||
let commands: ComplexCommand[] = [];
|
||||
if (typeof toRun === 'string') {
|
||||
commands = [{ commandName: toRun }];
|
||||
} else if ('commandName' in toRun) {
|
||||
commands = [toRun as ComplexCommand];
|
||||
} else if ('commands' in toRun) {
|
||||
const commandsInput = (toRun as Commands).commands;
|
||||
commands = Array.isArray(commandsInput)
|
||||
? commandsInput.map(cmd => (typeof cmd === 'string' ? { commandName: cmd } : cmd))
|
||||
: [{ commandName: commandsInput }];
|
||||
} else if (Array.isArray(toRun)) {
|
||||
commands = toRun.map(cmd => (typeof cmd === 'string' ? { commandName: cmd } : cmd));
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
console.log("Command isn't runnable", toRun);
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
(commands as Command[]).forEach(({ commandName, commandOptions, context }) => {
|
||||
// Execute each command in the array
|
||||
let result: unknown;
|
||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||
if (commandName) {
|
||||
result = this.runCommand(
|
||||
commandName,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import objectHash from 'object-hash';
|
||||
import log from '../log.js';
|
||||
import { hotkeys } from '../utils';
|
||||
import isequal from 'lodash.isequal';
|
||||
import Hotkey from './Hotkey';
|
||||
@ -276,10 +275,3 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
export default HotkeysManager;
|
||||
|
||||
// Commands Contexts:
|
||||
|
||||
// --> Name and Priority
|
||||
// GLOBAL: 0
|
||||
// VIEWER::CORNERSTONE: 1
|
||||
// VIEWER::VTK: 1
|
||||
|
||||
@ -247,7 +247,6 @@ describe('ExtensionManager.ts', () => {
|
||||
definitions: {
|
||||
exampleDefinition: {
|
||||
commandFn: () => {},
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
|
||||
@ -45,6 +45,7 @@ export interface Extension {
|
||||
getUtilityModule?: (p: ExtensionParams) => unknown;
|
||||
getCustomizationModule?: (p: ExtensionParams) => unknown;
|
||||
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
|
||||
getToolbarModule?: (p: ExtensionParams) => unknown;
|
||||
onModeEnter?: () => void;
|
||||
onModeExit?: () => void;
|
||||
}
|
||||
@ -65,9 +66,24 @@ export default class ExtensionManager extends PubSubService {
|
||||
ACTIVE_DATA_SOURCE_CHANGED: 'event::activedatasourcechanged',
|
||||
};
|
||||
|
||||
public static readonly MODULE_TYPES = MODULE_TYPES;
|
||||
|
||||
private _commandsManager: CommandsManager;
|
||||
private _servicesManager: ServicesManager;
|
||||
private _hotkeysManager: HotkeysManager;
|
||||
private modulesMap: Record<string, unknown>;
|
||||
private modules: Record<string, any[]>;
|
||||
private registeredExtensionIds: string[];
|
||||
private moduleTypeNames: string[];
|
||||
private _appConfig: any;
|
||||
private _extensionLifeCycleHooks: {
|
||||
onModeEnter: Record<string, any>;
|
||||
onModeExit: Record<string, any>;
|
||||
};
|
||||
private dataSourceMap: Record<string, any>;
|
||||
private dataSourceDefs: Record<string, any>;
|
||||
private defaultDataSourceName: string;
|
||||
private activeDataSource: string;
|
||||
|
||||
constructor({
|
||||
commandsManager,
|
||||
@ -265,57 +281,74 @@ export default class ExtensionManager extends PubSubService {
|
||||
configuration
|
||||
);
|
||||
|
||||
if (extensionModule) {
|
||||
switch (moduleType) {
|
||||
case MODULE_TYPES.COMMANDS:
|
||||
this._initCommandsModule(extensionModule);
|
||||
break;
|
||||
case MODULE_TYPES.DATA_SOURCE:
|
||||
this._initDataSourcesModule(extensionModule, extensionId, dataSources);
|
||||
break;
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
this._initHangingProtocolsModule(extensionModule, extensionId);
|
||||
case MODULE_TYPES.TOOLBAR:
|
||||
case MODULE_TYPES.VIEWPORT:
|
||||
case MODULE_TYPES.PANEL:
|
||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||
case MODULE_TYPES.CONTEXT:
|
||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||
case MODULE_TYPES.CUSTOMIZATION:
|
||||
case MODULE_TYPES.STATE_SYNC:
|
||||
case MODULE_TYPES.UTILITY:
|
||||
// Default for most extension points,
|
||||
// Just adds each entry ready for consumption by mode.
|
||||
extensionModule.forEach(element => {
|
||||
if (!element.name) {
|
||||
throw new Error(
|
||||
`Extension ID ${extensionId} module ${moduleType} element has no name`
|
||||
);
|
||||
}
|
||||
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||
element.id = id;
|
||||
this.modulesMap[id] = element;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Module type invalid: ${moduleType}`);
|
||||
}
|
||||
|
||||
this.modules[moduleType].push({
|
||||
extensionId,
|
||||
module: extensionModule,
|
||||
});
|
||||
if (!extensionModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (moduleType) {
|
||||
case MODULE_TYPES.COMMANDS:
|
||||
this._initCommandsModule(extensionModule);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.DATA_SOURCE:
|
||||
this._initDataSourcesModule(extensionModule, extensionId, dataSources);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
this._initHangingProtocolsModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.PANEL:
|
||||
this._initPanelModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.TOOLBAR:
|
||||
this._initToolbarModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.VIEWPORT:
|
||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||
case MODULE_TYPES.CONTEXT:
|
||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||
case MODULE_TYPES.CUSTOMIZATION:
|
||||
case MODULE_TYPES.STATE_SYNC:
|
||||
case MODULE_TYPES.UTILITY:
|
||||
this.processExtensionModule(extensionModule, extensionId, moduleType);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Module type invalid: ${moduleType}`);
|
||||
}
|
||||
|
||||
this.modules[moduleType].push({
|
||||
extensionId,
|
||||
module: extensionModule,
|
||||
});
|
||||
});
|
||||
|
||||
// Track extension registration
|
||||
this.registeredExtensionIds.push(extensionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the module entry associated with the given string entry
|
||||
* @param stringEntry - The string entry to retrieve the module entry for which is
|
||||
* in the format of `${extensionId}.${moduleType}.${moduleName}`
|
||||
* @returns The module entry associated with the given string entry.
|
||||
*/
|
||||
getModuleEntry = stringEntry => {
|
||||
return this.modulesMap[stringEntry];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all modules of a given type for all registered extensions.
|
||||
*
|
||||
* @param moduleType - The type of modules to retrieve.
|
||||
* @returns An array of modules of the specified type.
|
||||
*/
|
||||
getModulesByType = (moduleType: string) => {
|
||||
return this.modules[moduleType];
|
||||
};
|
||||
|
||||
getDataSources = dataSourceName => {
|
||||
if (dataSourceName === undefined) {
|
||||
// Default to the activeDataSource
|
||||
@ -402,6 +435,38 @@ export default class ExtensionManager extends PubSubService {
|
||||
});
|
||||
};
|
||||
|
||||
_initPanelModule = (extensionModule, extensionId) => {
|
||||
this.processExtensionModule(extensionModule, extensionId, MODULE_TYPES.PANEL);
|
||||
};
|
||||
|
||||
_initToolbarModule = (extensionModule, extensionId) => {
|
||||
// check if the toolbar module has a handler function for evaluation of
|
||||
// the toolbar button state
|
||||
const { toolbarService } = this._servicesManager.services;
|
||||
extensionModule.forEach(toolbarButton => {
|
||||
if (toolbarButton.evaluate) {
|
||||
toolbarService.registerEvaluateFunction(toolbarButton.name, toolbarButton.evaluate);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes an extension module.
|
||||
* @param extensionModule - The extension module to process.
|
||||
* @param extensionId - The ID of the extension.
|
||||
* @param moduleType - The type of the module.
|
||||
*/
|
||||
private processExtensionModule(extensionModule, extensionId: string, moduleType: string) {
|
||||
extensionModule.forEach(element => {
|
||||
if (!element.name) {
|
||||
throw new Error(`Extension ID ${extensionId} module ${moduleType} element has no name`);
|
||||
}
|
||||
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||
element.id = id;
|
||||
this.modulesMap[id] = element;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given data source and optionally sets it as the active data source.
|
||||
* The method does this by first creating the data source.
|
||||
|
||||
58
platform/core/src/hooks/useToolbar.tsx
Normal file
58
platform/core/src/hooks/useToolbar.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export function useToolbar({ servicesManager, buttonSection = 'primary' }) {
|
||||
const { toolbarService, viewportGridService } = servicesManager.services;
|
||||
const { EVENTS } = toolbarService;
|
||||
|
||||
const [toolbarButtons, setToolbarButtons] = useState(
|
||||
toolbarService.getButtonSection(buttonSection)
|
||||
);
|
||||
|
||||
// Callback function for handling toolbar interactions
|
||||
const onInteraction = useCallback(
|
||||
args => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const refreshProps = {
|
||||
viewportId,
|
||||
};
|
||||
toolbarService.recordInteraction(args, {
|
||||
refreshProps,
|
||||
});
|
||||
},
|
||||
[toolbarService, viewportGridService]
|
||||
);
|
||||
|
||||
// Effect to handle toolbar modification events
|
||||
useEffect(() => {
|
||||
const handleToolbarModified = () => {
|
||||
setToolbarButtons(toolbarService.getButtonSection(buttonSection));
|
||||
};
|
||||
|
||||
const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => {
|
||||
return toolbarService.subscribe(event, handleToolbarModified);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subs.forEach(sub => sub.unsubscribe());
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
// Effect to handle active viewportId change event
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
viewportGridService.EVENTS.VIEWPORTS_READY,
|
||||
];
|
||||
|
||||
const subscriptions = events.map(event => {
|
||||
return viewportGridService.subscribe(event, ({ viewportId }) => {
|
||||
viewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
toolbarService.refreshToolbarState({ viewportId });
|
||||
});
|
||||
});
|
||||
|
||||
return () => subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}, [viewportGridService, toolbarService]);
|
||||
|
||||
return { toolbarButtons, onInteraction };
|
||||
}
|
||||
@ -45,6 +45,7 @@ describe('Top level exports', () => {
|
||||
'pubSubServiceInterface',
|
||||
'PubSubService',
|
||||
'PanelService',
|
||||
'useToolbar',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(OHIF).sort();
|
||||
|
||||
@ -12,6 +12,7 @@ import utils from './utils';
|
||||
import defaults from './defaults';
|
||||
import * as Types from './types';
|
||||
import * as Enums from './enums';
|
||||
import { useToolbar } from './hooks/useToolbar';
|
||||
|
||||
import {
|
||||
CineService,
|
||||
@ -81,6 +82,7 @@ const OHIF = {
|
||||
pubSubServiceInterface,
|
||||
PubSubService,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
export {
|
||||
@ -124,6 +126,7 @@ export {
|
||||
Enums,
|
||||
Types,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@ -1,82 +1,80 @@
|
||||
const name = 'CineService';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
getState: _getState,
|
||||
setCine: _setCine,
|
||||
setIsCineEnabled: _setIsCineEnabled,
|
||||
playClip: _playClip,
|
||||
stopClip: _stopClip,
|
||||
onModeExit: _onModeExit,
|
||||
setServiceImplementation,
|
||||
};
|
||||
class CineService extends PubSubService {
|
||||
public static readonly EVENTS = {
|
||||
CINE_STATE_CHANGED: 'event::cineStateChanged',
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
|
||||
_setCine: () => console.warn('setCine() NOT IMPLEMENTED'),
|
||||
_playClip: () => console.warn('playClip() NOT IMPLEMENTED'),
|
||||
_stopClip: () => console.warn('stopClip() NOT IMPLEMENTED'),
|
||||
_setIsCineEnabled: () => console.warn('setIsCineEnabled() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function _getState() {
|
||||
return serviceImplementation._getState();
|
||||
}
|
||||
|
||||
function _setCine({ id, frameRate, isPlaying }) {
|
||||
return serviceImplementation._setCine({ id, frameRate, isPlaying });
|
||||
}
|
||||
|
||||
function _setIsCineEnabled(isCineEnabled) {
|
||||
return serviceImplementation._setIsCineEnabled(isCineEnabled);
|
||||
}
|
||||
|
||||
function _playClip(element, playClipOptions) {
|
||||
return serviceImplementation._playClip(element, playClipOptions);
|
||||
}
|
||||
|
||||
function _stopClip(element) {
|
||||
return serviceImplementation._stopClip(element);
|
||||
}
|
||||
|
||||
function _onModeExit() {
|
||||
_setIsCineEnabled(false);
|
||||
}
|
||||
|
||||
function setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
setIsCineEnabled: setIsCineEnabledImplementation,
|
||||
playClip: playClipImplementation,
|
||||
stopClip: stopClipImplementation,
|
||||
}) {
|
||||
if (getStateImplementation) {
|
||||
serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (setCineImplementation) {
|
||||
serviceImplementation._setCine = setCineImplementation;
|
||||
}
|
||||
if (setIsCineEnabledImplementation) {
|
||||
serviceImplementation._setIsCineEnabled = setIsCineEnabledImplementation;
|
||||
}
|
||||
|
||||
if (playClipImplementation) {
|
||||
serviceImplementation._playClip = playClipImplementation;
|
||||
}
|
||||
|
||||
if (stopClipImplementation) {
|
||||
serviceImplementation._stopClip = stopClipImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
const CineService = {
|
||||
REGISTRATION: {
|
||||
altName: name,
|
||||
public static REGISTRATION = {
|
||||
name: 'cineService',
|
||||
altName: 'CineService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
return new CineService();
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
serviceImplementation = {};
|
||||
|
||||
constructor() {
|
||||
super(CineService.EVENTS);
|
||||
this.serviceImplementation = {};
|
||||
}
|
||||
|
||||
public getState() {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public setCine({ id, frameRate, isPlaying }) {
|
||||
return this.serviceImplementation._setCine({ id, frameRate, isPlaying });
|
||||
}
|
||||
|
||||
public setIsCineEnabled(isCineEnabled) {
|
||||
this.serviceImplementation._setIsCineEnabled(isCineEnabled);
|
||||
// Todo: for some reason i need to do this setTimeout since the
|
||||
// reducer state does not get updated right away and if we publish the
|
||||
// event and we use the cineService.getState() it will return the old state
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, isCineEnabled);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public playClip(element, playClipOptions) {
|
||||
return this.serviceImplementation._playClip(element, playClipOptions);
|
||||
}
|
||||
|
||||
public stopClip(element) {
|
||||
return this.serviceImplementation._stopClip(element);
|
||||
}
|
||||
|
||||
public _onModeExit() {
|
||||
this.setIsCineEnabled(false);
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
setIsCineEnabled: setIsCineEnabledImplementation,
|
||||
playClip: playClipImplementation,
|
||||
stopClip: stopClipImplementation,
|
||||
}) {
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (setCineImplementation) {
|
||||
this.serviceImplementation._setCine = setCineImplementation;
|
||||
}
|
||||
if (setIsCineEnabledImplementation) {
|
||||
this.serviceImplementation._setIsCineEnabled = setIsCineEnabledImplementation;
|
||||
}
|
||||
|
||||
if (playClipImplementation) {
|
||||
this.serviceImplementation._playClip = playClipImplementation;
|
||||
}
|
||||
|
||||
if (stopClipImplementation) {
|
||||
this.serviceImplementation._stopClip = stopClipImplementation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CineService;
|
||||
|
||||
@ -135,15 +135,15 @@ export default class CustomizationService extends PubSubService {
|
||||
return this.getModeCustomization(customizationId, defaultValue);
|
||||
}
|
||||
|
||||
/** Mode customizations are changes to the behaviour of the extensions
|
||||
/** Mode customizations are changes to the behavior of the extensions
|
||||
* when running in a given mode. Reset clears mode customizations.
|
||||
* Note that global customizations over-ride mode customizations.
|
||||
* @param defautlValue to return if no customization specified.
|
||||
* @param defaultValue to return if no customization specified.
|
||||
*/
|
||||
public getModeCustomization(
|
||||
customizationId: string,
|
||||
defaultValue?: Customization
|
||||
): Customization | void {
|
||||
): Customization {
|
||||
const customization =
|
||||
this.globalCustomizations[customizationId] ??
|
||||
this.modeCustomizations[customizationId] ??
|
||||
|
||||
@ -113,7 +113,7 @@ class MeasurementService extends PubSubService {
|
||||
public readonly VALUE_TYPES = VALUE_TYPES;
|
||||
|
||||
private measurements = new Map();
|
||||
private unmappedMeasurements = new Set();
|
||||
private unmappedMeasurements = new Map();
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
@ -485,7 +485,15 @@ class MeasurementService extends PubSubService {
|
||||
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
this.unmappedMeasurements.add(sourceAnnotationDetail.uid);
|
||||
// Todo: handle other
|
||||
this.unmappedMeasurements.set(sourceAnnotationDetail.uid, {
|
||||
...sourceAnnotationDetail,
|
||||
source: {
|
||||
name: source.name,
|
||||
version: source.version,
|
||||
uid: source.uid,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Failed to map', error);
|
||||
throw new Error(
|
||||
@ -567,10 +575,9 @@ class MeasurementService extends PubSubService {
|
||||
}
|
||||
|
||||
clearMeasurements() {
|
||||
this.unmappedMeasurements.clear();
|
||||
|
||||
// Make a copy of the measurements
|
||||
const measurements = [...this.measurements.values()];
|
||||
const measurements = [...this.measurements.values(), ...this.unmappedMeasurements.values()];
|
||||
this.unmappedMeasurements.clear();
|
||||
this.measurements.clear();
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import log from './../log.js';
|
||||
import Services from '../types/Services';
|
||||
import CommandsManager from '../classes/CommandsManager';
|
||||
import ExtensionManager from '../extensions/ExtensionManager';
|
||||
|
||||
export default class ServicesManager {
|
||||
public services: Services = {};
|
||||
public registeredServiceNames: string[] = [];
|
||||
private _commandsManager: CommandsManager;
|
||||
private _extensionManager: ExtensionManager;
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
@ -11,6 +15,10 @@ export default class ServicesManager {
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
|
||||
setExtensionManager(extensionManager) {
|
||||
this._extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new service.
|
||||
*
|
||||
@ -40,6 +48,7 @@ export default class ServicesManager {
|
||||
configuration,
|
||||
commandsManager: this._commandsManager,
|
||||
servicesManager: this,
|
||||
extensionManager: this._extensionManager,
|
||||
});
|
||||
if (service.altName) {
|
||||
console.log('Registering old name', service.altName);
|
||||
|
||||
@ -1,109 +1,79 @@
|
||||
import merge from 'lodash.merge';
|
||||
import { CommandsManager } from '../../classes';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import type { RunCommand, Commands } from '../../types/Command';
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
import ServicesManager from '../ServicesManager';
|
||||
import { Button, ButtonProps, EvaluateFunction, EvaluatePublic, NestedButtonProps } from './types';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified',
|
||||
TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified',
|
||||
};
|
||||
|
||||
export type ButtonListeners = Record<string, RunCommand>;
|
||||
|
||||
export interface ButtonProps {
|
||||
primary?: Button;
|
||||
secondary?: Button;
|
||||
items?: Button[];
|
||||
}
|
||||
|
||||
export interface Button extends Commands {
|
||||
id: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
type?: string;
|
||||
tooltip?: string;
|
||||
isActive?: boolean;
|
||||
listeners?: ButtonListeners;
|
||||
props?: ButtonProps;
|
||||
}
|
||||
|
||||
export interface ExtraButtonOptions {
|
||||
listeners?: ButtonListeners;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export default class ToolbarService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'toolbarService',
|
||||
// Note the old name is ToolBarService, with an upper B
|
||||
altName: 'ToolBarService',
|
||||
create: ({ commandsManager }) => {
|
||||
return new ToolbarService(commandsManager);
|
||||
create: ({ commandsManager, extensionManager, servicesManager }) => {
|
||||
return new ToolbarService(commandsManager, extensionManager, servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
public static _createButton(
|
||||
type: string,
|
||||
id: string,
|
||||
icon: string,
|
||||
label: string,
|
||||
commands: Command | Commands,
|
||||
tooltip?: string,
|
||||
extraOptions?: ExtraButtonOptions
|
||||
): Button {
|
||||
public static createButton(options: {
|
||||
id: string;
|
||||
label: string;
|
||||
commands: RunCommand;
|
||||
icon?: string;
|
||||
tooltip?: string;
|
||||
evaluate?: EvaluatePublic;
|
||||
listeners?: Record<string, RunCommand>;
|
||||
}): ButtonProps {
|
||||
const { id, icon, label, commands, tooltip, evaluate, listeners } = options;
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
type,
|
||||
commands,
|
||||
tooltip,
|
||||
...extraOptions,
|
||||
tooltip: tooltip || label,
|
||||
evaluate,
|
||||
listeners,
|
||||
};
|
||||
}
|
||||
|
||||
public static _createActionButton = ToolbarService._createButton.bind(null, 'action');
|
||||
public static _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
|
||||
public static _createToolButton = ToolbarService._createButton.bind(null, 'tool');
|
||||
|
||||
buttons: Record<string, Button> = {};
|
||||
state: {
|
||||
primaryToolId: string;
|
||||
toggles: Record<string, boolean>;
|
||||
groups: Record<string, unknown>;
|
||||
} = { primaryToolId: '', toggles: {}, groups: {} };
|
||||
|
||||
buttonSections: Record<string, unknown> = {
|
||||
/**
|
||||
* primary: ['Zoom', 'Wwwc'],
|
||||
* secondary: ['Length', 'RectangleRoi']
|
||||
*/
|
||||
// all buttons in the toolbar with their props
|
||||
buttons: Record<string, Button>;
|
||||
// the buttons in the toolbar, grouped by section, with their ids
|
||||
buttonSections: Record<string, string[]>;
|
||||
} = {
|
||||
buttons: {},
|
||||
buttonSections: {},
|
||||
};
|
||||
|
||||
_commandsManager: CommandsManager;
|
||||
extensionManager: ExtensionManager;
|
||||
_extensionManager: ExtensionManager;
|
||||
_servicesManager: ServicesManager;
|
||||
_evaluateFunction: Record<string, EvaluateFunction> = {};
|
||||
_serviceSubscriptions = [];
|
||||
|
||||
defaultTool: Record<string, unknown>;
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
constructor(
|
||||
commandsManager: CommandsManager,
|
||||
extensionManager: ExtensionManager,
|
||||
servicesManager: ServicesManager
|
||||
) {
|
||||
super(EVENTS);
|
||||
this._commandsManager = commandsManager;
|
||||
}
|
||||
|
||||
public init(extensionManager: ExtensionManager): void {
|
||||
this.extensionManager = extensionManager;
|
||||
this._extensionManager = extensionManager;
|
||||
this._servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.state = {
|
||||
primaryToolId: 'WindowLevel',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
buttons: {},
|
||||
buttonSections: {},
|
||||
};
|
||||
this.unsubscriptions = [];
|
||||
this.buttonSections = {};
|
||||
this.buttons = {};
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
@ -111,16 +81,52 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default tool that will be activated whenever the primary tool is
|
||||
* deactivated without activating another/different tool.
|
||||
* @param interaction the interaction command that will set the default tool active
|
||||
* Registers an evaluate function with the specified name.
|
||||
*
|
||||
* @param name - The name of the evaluate function.
|
||||
* @param handler - The evaluate function handler.
|
||||
*/
|
||||
public setDefaultTool(interaction) {
|
||||
this.defaultTool = interaction;
|
||||
public registerEvaluateFunction(name: string, handler: EvaluateFunction) {
|
||||
this._evaluateFunction[name] = handler;
|
||||
}
|
||||
|
||||
public getDefaultTool() {
|
||||
return this.defaultTool;
|
||||
/**
|
||||
* Registers a service and its event to listen for updates and refreshes the toolbar state when the event is triggered.
|
||||
* @param service - The service to register.
|
||||
* @param event - The event to listen for.
|
||||
*/
|
||||
public registerEventForToolbarUpdate(service, events) {
|
||||
const { viewportGridService } = this._servicesManager.services;
|
||||
const callback = () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
this.refreshToolbarState({ viewportId });
|
||||
};
|
||||
|
||||
const unsubscriptions = events.map(event => {
|
||||
if (service.subscribe) {
|
||||
return service.subscribe(event, callback);
|
||||
} else if (service.addEventListener) {
|
||||
return service.addEventListener(event, callback);
|
||||
}
|
||||
});
|
||||
|
||||
unsubscriptions.forEach(unsub => this._serviceSubscriptions.push(unsub));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds buttons to the toolbar.
|
||||
* @param buttons - The buttons to be added.
|
||||
*/
|
||||
public addButtons(buttons: Button[]): void {
|
||||
buttons.forEach(button => {
|
||||
if (!this.state.buttons[button.id]) {
|
||||
this.state.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
...this.state,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,160 +136,180 @@ export default class ToolbarService extends PubSubService {
|
||||
* used for calling the specified interaction. That is, the command is
|
||||
* called with {...commandOptions,...options}
|
||||
*/
|
||||
public recordInteraction(interaction, options?: Record<string, unknown>) {
|
||||
if (!interaction) {
|
||||
return;
|
||||
public recordInteraction(
|
||||
interaction,
|
||||
options?: {
|
||||
refreshProps: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
const commandsManager = this._commandsManager;
|
||||
const { groupId, itemId, commands, type } = interaction;
|
||||
let { interactionType } = interaction;
|
||||
|
||||
// if not interaction type, assume the type can be used
|
||||
if (!interactionType) {
|
||||
interactionType = type;
|
||||
) {
|
||||
// if interaction is a string, we can assume it is the itemId
|
||||
// and get the props to get the other properties
|
||||
if (typeof interaction === 'string') {
|
||||
interaction = this.getButtonProps(interaction);
|
||||
}
|
||||
|
||||
switch (interactionType) {
|
||||
case 'action': {
|
||||
commandsManager.run(commands, options);
|
||||
break;
|
||||
}
|
||||
case 'tool': {
|
||||
try {
|
||||
const alternateInteraction =
|
||||
this.state.primaryToolId === itemId &&
|
||||
this.defaultTool?.itemId !== itemId &&
|
||||
this.getDefaultTool();
|
||||
if (alternateInteraction) {
|
||||
// Allow toggling the mode off
|
||||
return this.recordInteraction(alternateInteraction, options);
|
||||
}
|
||||
commandsManager.run(commands, options);
|
||||
const itemId = interaction.itemId ?? interaction.id;
|
||||
interaction.itemId = itemId;
|
||||
|
||||
// only set the primary tool if no error was thrown.
|
||||
// if the itemId is not undefined use it; otherwise, set the first tool in
|
||||
// the commands as the primary tool
|
||||
this.state.primaryToolId = itemId || commands[0].commandOptions?.toolName;
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
const commands = Array.isArray(interaction.commands)
|
||||
? interaction.commands
|
||||
: [interaction.commands];
|
||||
|
||||
break;
|
||||
}
|
||||
case 'toggle': {
|
||||
const { commands } = interaction;
|
||||
let commandExecuted;
|
||||
|
||||
// only toggle if a command was executed
|
||||
this.state.toggles[itemId] =
|
||||
this.state.toggles[itemId] === undefined ? true : !this.state.toggles[itemId];
|
||||
|
||||
if (!commands) {
|
||||
break;
|
||||
}
|
||||
|
||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||
if (!commandOptions) {
|
||||
commandOptions = {};
|
||||
}
|
||||
|
||||
if (commandName) {
|
||||
commandOptions.toggledState = this.state.toggles[itemId];
|
||||
|
||||
try {
|
||||
commandsManager.runCommand(commandName, commandOptions, context);
|
||||
commandExecuted = true;
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!commandExecuted) {
|
||||
// If no command was executed, we need to toggle the state back
|
||||
this.state.toggles[itemId] = !this.state.toggles[itemId];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid interaction type: ${interactionType}`);
|
||||
}
|
||||
|
||||
// Todo: comment out for now
|
||||
// Run command if there's one associated
|
||||
//
|
||||
// NOTE: Should probably just do this for tools as well?
|
||||
// But would be nice if we could enforce at least the command name?
|
||||
// let unsubscribe;
|
||||
// if (commandName) {
|
||||
// unsubscribe = commandsManager.runCommand(commandName, commandOptions);
|
||||
// }
|
||||
|
||||
// // Storing the unsubscribe for later resetting
|
||||
// if (unsubscribe && typeof unsubscribe === 'function') {
|
||||
// if (this.unsubscriptions.indexOf(unsubscribe) === -1) {
|
||||
// this.unsubscriptions.push(unsubscribe);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Track last touched id for each group
|
||||
if (groupId) {
|
||||
this.state.groups[groupId] = itemId;
|
||||
}
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
getButtons() {
|
||||
return this.buttons;
|
||||
}
|
||||
|
||||
getActiveTools() {
|
||||
const activeTools = [this.state.primaryToolId];
|
||||
Object.keys(this.state.toggles).forEach(key => {
|
||||
if (this.state.toggles[key]) {
|
||||
activeTools.push(key);
|
||||
}
|
||||
});
|
||||
return activeTools;
|
||||
}
|
||||
|
||||
getActivePrimaryTool() {
|
||||
return this.state.primaryToolId;
|
||||
}
|
||||
|
||||
/** Sets the toggle state of a button to the isToggled state */
|
||||
public setToggled(id: string, isToggled: boolean): void {
|
||||
if (isToggled) {
|
||||
this.state.toggles[id] = true;
|
||||
} else {
|
||||
delete this.state.toggles[id];
|
||||
}
|
||||
}
|
||||
|
||||
setButton(id, button) {
|
||||
if (this.buttons[id]) {
|
||||
this.buttons[id] = merge(this.buttons[id], button);
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
buttons: this.buttons,
|
||||
button: this.buttons[id],
|
||||
buttonSections: this.buttonSections,
|
||||
if (!commands?.length) {
|
||||
this.refreshToolbarState({
|
||||
...options?.refreshProps,
|
||||
itemId,
|
||||
interaction,
|
||||
});
|
||||
}
|
||||
|
||||
const commandOptions = { ...options, ...interaction };
|
||||
|
||||
// Loop through commands and run them with the combined options
|
||||
this._commandsManager.run(commands, commandOptions);
|
||||
|
||||
this.refreshToolbarState({
|
||||
...options?.refreshProps,
|
||||
itemId,
|
||||
interaction,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidates the state of the toolbar after an interaction, it accepts
|
||||
* props that get passed to the buttons
|
||||
*
|
||||
* @param refreshProps - The props that buttons need to get evaluated, they can be
|
||||
* { viewportId, toolGroup} for cornerstoneTools.
|
||||
*
|
||||
* Todo: right now refreshToolbarState should be used in the context where
|
||||
* we have access to the toolGroup and viewportId, but we should be able to
|
||||
* pass the props to the toolbar service and it should be able to decide
|
||||
* which buttons to evaluate based on the props
|
||||
*/
|
||||
public refreshToolbarState(refreshProps) {
|
||||
const buttons = this.state.buttons;
|
||||
|
||||
// Tracks evaluated buttons to avoid re-evaluating them (this will
|
||||
// cause issue for toggles where if the button is in primary
|
||||
// and secondary it will be evaluated twice)
|
||||
const evaluationResults = new Map();
|
||||
|
||||
const evaluateButtonProps = (button, props, refreshProps) => {
|
||||
if (evaluationResults.has(button.id)) {
|
||||
const { disabled, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, className, isActive };
|
||||
} else {
|
||||
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
||||
const updatedProps = {
|
||||
...props,
|
||||
disabled: evaluated?.disabled || false,
|
||||
className: evaluated?.className || '',
|
||||
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
|
||||
};
|
||||
evaluationResults.set(button.id, updatedProps);
|
||||
return updatedProps;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshedButtons = Object.values(buttons).reduce((acc, button: Button) => {
|
||||
const isNested = (button.props as NestedButtonProps)?.groupId;
|
||||
|
||||
if (!isNested) {
|
||||
this.handleEvaluate(button.props);
|
||||
const buttonProps = button.props as ButtonProps;
|
||||
|
||||
const updatedProps = evaluateButtonProps(button, buttonProps, refreshProps);
|
||||
acc[button.id] = {
|
||||
...button,
|
||||
props: updatedProps,
|
||||
};
|
||||
} else {
|
||||
let buttonProps = button.props as NestedButtonProps;
|
||||
// if it is nested we should perform evaluate on each item in the group
|
||||
this.handleEvaluateNested(buttonProps);
|
||||
|
||||
const { evaluate: groupEvaluate } = buttonProps;
|
||||
|
||||
const groupEvaluated = groupEvaluate?.({ ...refreshProps, button });
|
||||
// handle group evaluate function which might switch the primary
|
||||
// item in the group
|
||||
buttonProps = {
|
||||
...buttonProps,
|
||||
primary: groupEvaluated?.primary || buttonProps.primary,
|
||||
};
|
||||
|
||||
const { primary, items } = buttonProps;
|
||||
|
||||
// primary and items evaluate functions
|
||||
let updatedPrimary;
|
||||
if (primary) {
|
||||
updatedPrimary = evaluateButtonProps(primary, primary, refreshProps);
|
||||
}
|
||||
const updatedItems = items.map(item => evaluateButtonProps(item, item, refreshProps));
|
||||
buttonProps = {
|
||||
...buttonProps,
|
||||
primary: updatedPrimary,
|
||||
items: updatedItems,
|
||||
};
|
||||
|
||||
acc[button.id] = {
|
||||
...button,
|
||||
props: buttonProps,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.setButtons(refreshedButtons);
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the buttons for the toolbar, don't use this method to record an
|
||||
* interaction, since it doesn't update the state of the buttons, use
|
||||
* this if you know the buttons you want to set and you want to set them
|
||||
* all at once.
|
||||
* @param buttons - The buttons to set.
|
||||
*/
|
||||
public setButtons(buttons) {
|
||||
this.state.buttons = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
buttons: this.state.buttons,
|
||||
buttonSections: this.state.buttonSections,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a button by its ID.
|
||||
* @param id - The ID of the button to retrieve.
|
||||
* @returns The button with the specified ID.
|
||||
*/
|
||||
public getButton(id: string): Button {
|
||||
return this.buttons[id];
|
||||
return this.state.buttons[id];
|
||||
}
|
||||
|
||||
/** Gets a nested button, found in the items/props for the children */
|
||||
public getNestedButton(id: string): Button {
|
||||
if (this.buttons[id]) {
|
||||
return this.buttons[id];
|
||||
}
|
||||
for (const buttonId of Object.keys(this.buttons)) {
|
||||
const { primary, items } = this.buttons[buttonId].props || {};
|
||||
/**
|
||||
* Retrieves the buttons from the toolbar service.
|
||||
* @returns An array of buttons.
|
||||
*/
|
||||
public getButtons() {
|
||||
return this.state.buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the button properties for the specified button ID.
|
||||
* It prioritizes nested buttons over regular buttons if the ID is found
|
||||
* in both.
|
||||
*
|
||||
* @param id - The ID of the button.
|
||||
* @returns The button properties.
|
||||
*/
|
||||
public getButtonProps(id: string): ButtonProps {
|
||||
for (const buttonId of Object.keys(this.state.buttons)) {
|
||||
const { primary, items } = (this.state.buttons[buttonId].props as NestedButtonProps) || {};
|
||||
if (primary?.id === id) {
|
||||
return primary;
|
||||
}
|
||||
@ -292,97 +318,57 @@ export default class ToolbarService extends PubSubService {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
// This should be checked after we checked the nested buttons, since
|
||||
// we are checking based on the ids, the nested objects are higher priority
|
||||
// and more specific
|
||||
if (this.state.buttons[id]) {
|
||||
return this.state.buttons[id].props as ButtonProps;
|
||||
}
|
||||
}
|
||||
|
||||
setButtons(buttons) {
|
||||
this.buttons = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
buttons: this.buttons,
|
||||
buttonSections: this.buttonSections,
|
||||
});
|
||||
}
|
||||
_getButtonUITypes() {
|
||||
const registeredToolbarModules = this._extensionManager.modules['toolbarModule'];
|
||||
|
||||
_buttonTypes() {
|
||||
const buttonTypes = {};
|
||||
const registeredToolbarModules = this.extensionManager.modules['toolbarModule'];
|
||||
|
||||
if (Array.isArray(registeredToolbarModules) && registeredToolbarModules.length) {
|
||||
registeredToolbarModules.forEach(toolbarModule =>
|
||||
toolbarModule.module.forEach(def => {
|
||||
buttonTypes[def.name] = def;
|
||||
})
|
||||
);
|
||||
if (!Array.isArray(registeredToolbarModules)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return buttonTypes;
|
||||
}
|
||||
|
||||
createButtonSection(key, buttons) {
|
||||
// Maybe do this mapping at time of return, instead of time of create
|
||||
// Props check important for validation here...
|
||||
|
||||
this.buttonSections[key] = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Finds a button section by it's name/tool group id, then maps the list of string name
|
||||
* identifiers to schema/values that can be used to render the buttons.
|
||||
*
|
||||
* @param toolGroupId - the tool group id
|
||||
* @param props - optional properties to apply to every button of the section
|
||||
* @param defaultToolGroupId - the fallback section to return if the given toolGroupId section is not available
|
||||
*/
|
||||
getButtonSection(
|
||||
toolGroupId: string,
|
||||
props?: Record<string, unknown>,
|
||||
defaultToolGroupId = 'primary'
|
||||
) {
|
||||
const buttonSectionIds =
|
||||
this.buttonSections[toolGroupId] || this.buttonSections[defaultToolGroupId];
|
||||
const buttonsInSection = [];
|
||||
|
||||
if (buttonSectionIds && buttonSectionIds.length !== 0) {
|
||||
buttonSectionIds.forEach(btnId => {
|
||||
const btn = this.buttons[btnId];
|
||||
const metadata = {};
|
||||
const mappedBtn = this._mapButtonToDisplay(btn, toolGroupId, metadata, props);
|
||||
|
||||
buttonsInSection.push(mappedBtn);
|
||||
return registeredToolbarModules.reduce((buttonTypes, toolbarModule) => {
|
||||
toolbarModule.module.forEach(def => {
|
||||
buttonTypes[def.name] = def;
|
||||
});
|
||||
}
|
||||
|
||||
return buttonsInSection;
|
||||
return buttonTypes;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object[]} buttons
|
||||
* @param {string} buttons[].id
|
||||
* Creates a button section with the specified key and buttons.
|
||||
* @param {string} key - The key of the button section.
|
||||
* @param {Array} buttons - The buttons to be added to the section.
|
||||
*/
|
||||
addButtons(buttons) {
|
||||
buttons.forEach(button => {
|
||||
if (!this.buttons[button.id]) {
|
||||
this.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
this._setTogglesForButtonItems(buttons);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
createButtonSection(key, buttons) {
|
||||
this.state.buttonSections[key] = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
_setTogglesForButtonItems(buttons) {
|
||||
if (!buttons) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Retrieves the button section with the specified sectionId.
|
||||
*
|
||||
* @param sectionId - The ID of the button section to retrieve.
|
||||
* @param props - Optional additional properties for mapping the button to display.
|
||||
* @returns An array of buttons in the specified section, mapped to their display representation.
|
||||
*/
|
||||
getButtonSection(sectionId: string, props?: Record<string, unknown>) {
|
||||
const buttonSectionIds = this.state.buttonSections[sectionId];
|
||||
|
||||
buttons.forEach(buttonItem => {
|
||||
if (buttonItem.type === 'toggle') {
|
||||
this.setToggled(buttonItem.id, buttonItem.isActive);
|
||||
}
|
||||
this._setTogglesForButtonItems(buttonItem.props?.items);
|
||||
});
|
||||
return (
|
||||
buttonSectionIds?.map(btnId => {
|
||||
const btn = this.state.buttons[btnId];
|
||||
return this._mapButtonToDisplay(btn, props);
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -392,18 +378,22 @@ export default class ToolbarService extends PubSubService {
|
||||
* @param {*} metadata
|
||||
* @param {*} props - Props set by the Viewer layer
|
||||
*/
|
||||
_mapButtonToDisplay(btn, btnSection, metadata, props) {
|
||||
_mapButtonToDisplay(btn, props) {
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, type, component } = btn;
|
||||
const buttonType = this._buttonTypes()[type];
|
||||
const { id, uiType, component } = btn;
|
||||
const { groupId } = btn.props;
|
||||
|
||||
const buttonType = this._getButtonUITypes()[uiType];
|
||||
|
||||
if (!buttonType) {
|
||||
return;
|
||||
}
|
||||
|
||||
!groupId ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
|
||||
|
||||
return {
|
||||
id,
|
||||
Component: component || buttonType.defaultComponent,
|
||||
@ -411,7 +401,54 @@ export default class ToolbarService extends PubSubService {
|
||||
};
|
||||
}
|
||||
|
||||
handleEvaluateNested = props => {
|
||||
const { primary, items } = props;
|
||||
// handle group evaluate function
|
||||
this.handleEvaluate(props);
|
||||
|
||||
// primary and items evaluate functions
|
||||
if (primary) {
|
||||
this.handleEvaluate(primary);
|
||||
}
|
||||
items.forEach(item => this.handleEvaluate(item));
|
||||
};
|
||||
|
||||
handleEvaluate = props => {
|
||||
const { evaluate } = props;
|
||||
|
||||
if (typeof evaluate === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof evaluate === 'string') {
|
||||
const evaluateFunction = this._evaluateFunction[evaluate];
|
||||
|
||||
if (evaluateFunction) {
|
||||
props.evaluate = evaluateFunction;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Evaluate function not found for name: ${evaluate}, you can register an evaluate function with the getToolbarModule in your extensions`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof evaluate === 'object') {
|
||||
const { name, options } = evaluate;
|
||||
const evaluateFunction = this._evaluateFunction[name];
|
||||
|
||||
if (evaluateFunction) {
|
||||
props.evaluate = args => evaluateFunction({ ...args, ...options });
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Evaluate function not found for name: ${name}, you can register an evaluate function with the getToolbarModule in your extensions`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getButtonComponentForUIType(uiType: string) {
|
||||
return uiType ? this._buttonTypes()[uiType]?.defaultComponent ?? null : null;
|
||||
return uiType ? this._getButtonUITypes()[uiType]?.defaultComponent ?? null : null;
|
||||
}
|
||||
}
|
||||
|
||||
43
platform/core/src/services/ToolBarService/types.ts
Normal file
43
platform/core/src/services/ToolBarService/types.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
|
||||
export type EvaluatePublic = string | EvaluateFunction;
|
||||
|
||||
export type EvaluateFunction = (props: Record<string, unknown>) => {
|
||||
disabled: boolean;
|
||||
className: string;
|
||||
};
|
||||
|
||||
export type ButtonProps = {
|
||||
id: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
commands?: RunCommand;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
evaluate?: EvaluatePublic;
|
||||
listeners?: Record<string, RunCommand>;
|
||||
};
|
||||
|
||||
export type NestedButtonProps = {
|
||||
groupId: string;
|
||||
// group evaluate which is different
|
||||
// from the evaluate function for the primary and items
|
||||
evaluate?: EvaluatePublic;
|
||||
items: ButtonProps[];
|
||||
primary: ButtonProps & {
|
||||
// Todo: this is really ugly but really we don't have any other option
|
||||
// the ui design requires this since the button should be rounded if
|
||||
// active otherwise it should not be rounded
|
||||
isActive?: boolean;
|
||||
};
|
||||
secondary: ButtonProps;
|
||||
};
|
||||
|
||||
export type Button = {
|
||||
id: string;
|
||||
props: ButtonProps | NestedButtonProps;
|
||||
// button ui type (e.g. 'ohif.splitButton', 'ohif.radioGroup')
|
||||
// extensions can provide custom components for these types
|
||||
uiType: string;
|
||||
};
|
||||
@ -7,6 +7,7 @@ class ViewportGridService extends PubSubService {
|
||||
LAYOUT_CHANGED: 'event::layoutChanged',
|
||||
GRID_STATE_CHANGED: 'event::gridStateChanged',
|
||||
GRID_SIZE_CHANGED: 'event::gridSizeChanged',
|
||||
VIEWPORTS_READY: 'event::viewportsReady',
|
||||
};
|
||||
|
||||
public static REGISTRATION = {
|
||||
@ -35,6 +36,7 @@ class ViewportGridService extends PubSubService {
|
||||
onModeExit: onModeExitImplementation,
|
||||
set: setImplementation,
|
||||
getNumViewportPanes: getNumViewportPanesImplementation,
|
||||
setViewportIsReady: setViewportIsReadyImplementation,
|
||||
}): void {
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
@ -61,6 +63,14 @@ class ViewportGridService extends PubSubService {
|
||||
if (getNumViewportPanesImplementation) {
|
||||
this.serviceImplementation._getNumViewportPanes = getNumViewportPanesImplementation;
|
||||
}
|
||||
|
||||
if (setViewportIsReadyImplementation) {
|
||||
this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
public publishViewportsReady() {
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORTS_READY, {});
|
||||
}
|
||||
|
||||
public setActiveViewportId(id: string) {
|
||||
@ -74,6 +84,10 @@ class ViewportGridService extends PubSubService {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public setViewportIsReady(viewportId, callback) {
|
||||
this.serviceImplementation._setViewportIsReady(viewportId, callback);
|
||||
}
|
||||
|
||||
public getActiveViewportId() {
|
||||
const state = this.getState();
|
||||
return state.activeViewportId;
|
||||
@ -110,6 +124,17 @@ class ViewportGridService extends PubSubService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the display set instance UIDs for a given viewport.
|
||||
* @param viewportId The ID of the viewport.
|
||||
* @returns An array of display set instance UIDs.
|
||||
*/
|
||||
public getDisplaySetsUIDsForViewport(viewportId: string) {
|
||||
const state = this.getState();
|
||||
const viewport = state.viewports.get(viewportId);
|
||||
return viewport?.displaySetInstanceUIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param numCols, numRows - the number of columns and rows to apply
|
||||
|
||||
@ -13,11 +13,11 @@ import HangingProtocolService from './HangingProtocolService';
|
||||
import pubSubServiceInterface, { PubSubService } from './_shared/pubSubServiceInterface';
|
||||
import UserAuthenticationService from './UserAuthenticationService';
|
||||
import CustomizationService from './CustomizationService';
|
||||
|
||||
import Services from '../types/Services';
|
||||
import StateSyncService from './StateSyncService';
|
||||
import PanelService from './PanelService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
export {
|
||||
Services,
|
||||
MeasurementService,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user