feature(segmentation): Add contour tools and utilities in a side panel (#5517)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
@ -3,7 +3,7 @@ import { classes, Types } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default';
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
|
||||
@ -37,7 +37,7 @@ const commandsModule = ({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const { segmentationService, displaySetService, viewportGridService, toolGroupService } =
|
||||
const { segmentationService, displaySetService, viewportGridService } =
|
||||
servicesManager.services as AppTypes.Services;
|
||||
|
||||
const actions = {
|
||||
@ -307,6 +307,17 @@ const commandsModule = ({
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
toggleActiveSegmentationUtility: ({ itemId: buttonId }) => {
|
||||
const { uiState, setUIState } = useUIStateStore.getState();
|
||||
const isButtonActive = uiState['activeSegmentationUtility'] === buttonId;
|
||||
console.log('toggleActiveSegmentationUtility', isButtonActive, buttonId);
|
||||
// if the button is active, clear the active segmentation utility
|
||||
if (isButtonActive) {
|
||||
setUIState('activeSegmentationUtility', null);
|
||||
} else {
|
||||
setUIState('activeSegmentationUtility', buttonId);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -326,6 +337,9 @@ const commandsModule = ({
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
toggleActiveSegmentationUtility: {
|
||||
commandFn: actions.toggleActiveSegmentationUtility,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,243 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useRunCommand, useSystem } from '@ohif/core';
|
||||
import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone';
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Separator,
|
||||
Switch,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@ohif/ui-next';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import { contourSegmentation } from '@cornerstonejs/tools/utilities';
|
||||
import { Segment } from '@cornerstonejs/tools/types';
|
||||
|
||||
const { LogicalOperation } = contourSegmentation;
|
||||
const options = [
|
||||
{
|
||||
value: 'merge',
|
||||
logicalOperation: LogicalOperation.Union,
|
||||
label: 'Merge',
|
||||
icon: 'actions-combine-merge',
|
||||
helperIcon: 'helper-combine-merge',
|
||||
},
|
||||
{
|
||||
value: 'intersect',
|
||||
logicalOperation: LogicalOperation.Intersect,
|
||||
label: 'Intersect',
|
||||
icon: 'actions-combine-intersect',
|
||||
helperIcon: 'helper-combine-intersect',
|
||||
},
|
||||
{
|
||||
value: 'subtract',
|
||||
logicalOperation: LogicalOperation.Subtract,
|
||||
label: 'Subtract',
|
||||
icon: 'actions-combine-subtract',
|
||||
helperIcon: 'helper-combine-subtract',
|
||||
},
|
||||
];
|
||||
|
||||
// Shared component for segment selection
|
||||
function SegmentSelector({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
segments,
|
||||
placeholder = 'Select a segment',
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
segments: Segment[];
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-between gap-6">
|
||||
<div>{label}</div>
|
||||
<Select
|
||||
key={`select-segment-${label}`}
|
||||
onValueChange={onValueChange}
|
||||
value={value}
|
||||
>
|
||||
<SelectTrigger className="overflow-hidden">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{segments.map(segment => (
|
||||
<SelectItem
|
||||
key={segment.segmentIndex}
|
||||
value={segment.segmentIndex.toString()}
|
||||
>
|
||||
{segment.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LogicalContourOperationOptions() {
|
||||
const { servicesManager } = useSystem();
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { segmentationsWithRepresentations } = useActiveViewportSegmentationRepresentations();
|
||||
|
||||
const activeRepresentation = segmentationsWithRepresentations?.find(
|
||||
({ representation }) => representation?.active
|
||||
);
|
||||
|
||||
const segments = activeRepresentation
|
||||
? Object.values(activeRepresentation.segmentation.segments)
|
||||
: [];
|
||||
|
||||
// Calculate the next available segment index
|
||||
const nextSegmentIndex = activeRepresentation
|
||||
? segmentationService.getNextAvailableSegmentIndex(
|
||||
activeRepresentation.segmentation.segmentationId
|
||||
)
|
||||
: 1;
|
||||
|
||||
const activeSegment = segments.find(segment => segment.active);
|
||||
|
||||
const activeSegmentIndex = activeSegment?.segmentIndex || 0;
|
||||
|
||||
const [operation, setOperation] = useState(options[0]);
|
||||
const [segmentA, setSegmentA] = useState<string>(activeSegmentIndex?.toString() || '');
|
||||
const [segmentB, setSegmentB] = useState<string>('');
|
||||
const [createNewSegment, setCreateNewSegment] = useState<boolean>(false);
|
||||
const [newSegmentName, setNewSegmentName] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
setSegmentA(activeSegmentIndex?.toString() || null);
|
||||
}, [activeSegmentIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
setNewSegmentName(`Segment ${nextSegmentIndex}`);
|
||||
}, [nextSegmentIndex]);
|
||||
|
||||
const runCommand = useRunCommand();
|
||||
|
||||
const applyLogicalContourOperation = useCallback(() => {
|
||||
let resultSegmentIndex = segmentA;
|
||||
if (createNewSegment) {
|
||||
resultSegmentIndex = nextSegmentIndex.toString();
|
||||
runCommand('addSegment', {
|
||||
segmentationId: activeRepresentation.segmentation.segmentationId,
|
||||
config: {
|
||||
label: newSegmentName,
|
||||
segmentIndex: nextSegmentIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
runCommand('applyLogicalContourOperation', {
|
||||
segmentAInfo: {
|
||||
segmentationId: activeRepresentation.segmentation.segmentationId,
|
||||
segmentIndex: parseInt(segmentA),
|
||||
},
|
||||
segmentBInfo: {
|
||||
segmentationId: activeRepresentation.segmentation.segmentationId,
|
||||
segmentIndex: parseInt(segmentB),
|
||||
},
|
||||
resultSegmentInfo: {
|
||||
segmentationId: activeRepresentation.segmentation.segmentationId,
|
||||
segmentIndex: parseInt(resultSegmentIndex),
|
||||
},
|
||||
logicalOperation: operation.logicalOperation,
|
||||
});
|
||||
}, [
|
||||
activeRepresentation?.segmentation?.segmentationId,
|
||||
createNewSegment,
|
||||
newSegmentName,
|
||||
nextSegmentIndex,
|
||||
operation.logicalOperation,
|
||||
runCommand,
|
||||
segmentA,
|
||||
segmentB,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex w-[245px] flex-col gap-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex w-auto flex-col items-center gap-2 text-base font-normal leading-none">
|
||||
<Tabs value={operation.value}>
|
||||
<TabsList className="inline-flex space-x-1">
|
||||
{options.map(option => {
|
||||
const { value, icon } = option;
|
||||
return (
|
||||
<TabsTrigger
|
||||
value={value}
|
||||
key={`logical-contour-operation-${value}`}
|
||||
onClick={() => setOperation(option)}
|
||||
>
|
||||
<Icons.ByName name={icon}></Icons.ByName>
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div>{operation.label}</div>
|
||||
</div>
|
||||
<div className="bg-primary-dark flex h-[62px] w-[88px] items-center justify-center rounded-lg">
|
||||
<Icons.ByName name={operation.helperIcon}></Icons.ByName>
|
||||
</div>
|
||||
</div>
|
||||
<SegmentSelector
|
||||
label="A"
|
||||
value={segmentA}
|
||||
onValueChange={setSegmentA}
|
||||
segments={segments}
|
||||
/>
|
||||
<SegmentSelector
|
||||
label="B"
|
||||
value={segmentB}
|
||||
onValueChange={setSegmentB}
|
||||
segments={segments}
|
||||
/>
|
||||
<div className="flex justify-end pl-[34px]">
|
||||
<Button
|
||||
className="border-primary/60 grow border"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
applyLogicalContourOperation();
|
||||
}}
|
||||
>
|
||||
{operation.label}
|
||||
</Button>
|
||||
</div>
|
||||
<Separator className="bg-input mt-2 h-[1px]" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Switch
|
||||
id="logical-contour-operations-create-new-segment-switch"
|
||||
onCheckedChange={setCreateNewSegment}
|
||||
></Switch>
|
||||
<Label htmlFor="logical-contour-operations-create-new-segment-switch">
|
||||
Create a new segment
|
||||
</Label>
|
||||
</div>
|
||||
<div className="pl-9">
|
||||
<Input
|
||||
className={cn(createNewSegment ? 'visible' : 'hidden')}
|
||||
disabled={!createNewSegment}
|
||||
id="logical-contour-operations-create-new-segment-input"
|
||||
type="text"
|
||||
placeholder="New segment name"
|
||||
value={newSegmentName}
|
||||
onChange={e => setNewSegmentName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogicalContourOperationOptions;
|
||||
@ -0,0 +1,71 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Input, Label, Separator } from '@ohif/ui-next';
|
||||
import { useRunCommand } from '@ohif/core';
|
||||
|
||||
function SimplifyContourOptions() {
|
||||
const [areaThreshold, setAreaThreshold] = useState(10);
|
||||
|
||||
const runCommand = useRunCommand();
|
||||
|
||||
return (
|
||||
<div className="flex w-auto w-[252px] flex-col gap-[8px] text-base font-normal leading-none">
|
||||
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
|
||||
<div>Fill contour holes</div>
|
||||
<Button
|
||||
className="border-primary/60 border"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
runCommand('removeContourHoles');
|
||||
}}
|
||||
>
|
||||
Fill Holes
|
||||
</Button>
|
||||
<Separator className="bg-input mt-[20px] h-[1px]" />
|
||||
</div>
|
||||
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
|
||||
<div>Remove Small Contours</div>
|
||||
<div className="flex items-center gap-2 self-end">
|
||||
<Label
|
||||
htmlFor="simplify-contour-options"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Area Threshold
|
||||
</Label>
|
||||
<Input
|
||||
id="simplify-contour-options"
|
||||
className="w-20"
|
||||
type="number"
|
||||
value={areaThreshold}
|
||||
onChange={e => setAreaThreshold(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="border-primary/60 border"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
runCommand('removeSmallContours', {
|
||||
areaThreshold,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Small Contours
|
||||
</Button>
|
||||
<Separator className="bg-input mt-[20px] h-[1px]" />
|
||||
</div>
|
||||
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
|
||||
<div>Create New Segment from Holes</div>
|
||||
<Button
|
||||
className="border-primary/60 border"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
runCommand('convertContourHoles');
|
||||
}}
|
||||
>
|
||||
Create New Segment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimplifyContourOptions;
|
||||
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Button, Separator } from '@ohif/ui-next';
|
||||
import { useRunCommand } from '@ohif/core';
|
||||
|
||||
function SmoothContoursOptions() {
|
||||
const runCommand = useRunCommand();
|
||||
|
||||
return (
|
||||
<div className="flex w-auto w-[245px] flex-col gap-[8px] text-base font-normal leading-none">
|
||||
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
|
||||
<div>Smooth all edges</div>
|
||||
<Button
|
||||
className="border-primary/60 border"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
runCommand('smoothContours');
|
||||
}}
|
||||
>
|
||||
Smooth Edges
|
||||
</Button>
|
||||
<Separator className="bg-input mt-[20px] h-[1px]" />
|
||||
</div>
|
||||
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
|
||||
<div>Remove extra points</div>
|
||||
<Button
|
||||
className="border-primary/60 border"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
runCommand('decimateContours');
|
||||
}}
|
||||
>
|
||||
Remove Points
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SmoothContoursOptions;
|
||||
@ -1,6 +1,32 @@
|
||||
import { useUIStateStore } from '@ohif/extension-default';
|
||||
import LogicalContourOperationsOptions from './components/LogicalContourOperationsOptions';
|
||||
import SimplifyContourOptions from './components/SimplifyContourOptions';
|
||||
import SmoothContoursOptions from './components/SmoothContoursOptions';
|
||||
|
||||
export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'cornerstone.SimplifyContourOptions',
|
||||
defaultComponent: SimplifyContourOptions,
|
||||
},
|
||||
{
|
||||
name: 'cornerstone.LogicalContourOperationsOptions',
|
||||
defaultComponent: LogicalContourOperationsOptions,
|
||||
},
|
||||
{
|
||||
name: 'cornerstone.SmoothContoursOptions',
|
||||
defaultComponent: SmoothContoursOptions,
|
||||
},
|
||||
{
|
||||
name: 'cornerstone.isActiveSegmentationUtility',
|
||||
evaluate: ({ button }) => {
|
||||
const { uiState } = useUIStateStore.getState();
|
||||
return {
|
||||
isActive: uiState[`activeSegmentationUtility`] === button.id,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentation',
|
||||
evaluate: ({ viewportId }) => {
|
||||
@ -10,6 +36,30 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
evaluate: ({ viewportId, segmentationRepresentationType }) => {
|
||||
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledText: 'No segmentations available',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!segmentations.some(segmentation =>
|
||||
Boolean(segmentation.type === segmentationRepresentationType)
|
||||
)
|
||||
) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledText: `No ${segmentationRepresentationType} segmentations available`,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
|
||||
@ -14,13 +14,18 @@ import {
|
||||
utilities as cstUtils,
|
||||
annotation,
|
||||
Types as ToolTypes,
|
||||
SplineContourSegmentationTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import {
|
||||
SegmentInfo,
|
||||
LogicalOperation,
|
||||
OperatorOptions,
|
||||
} from '@cornerstonejs/tools/utilities/contourSegmentation/logicalOperators';
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation';
|
||||
import { ONNXSegmentationController } from '@cornerstonejs/ai';
|
||||
|
||||
import { Types as OhifTypes, utils } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import {
|
||||
callInputDialogAutoComplete,
|
||||
createReportAsync,
|
||||
@ -33,7 +38,11 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u
|
||||
import { getViewportEnabledElement } from './utils/getViewportEnabledElement';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
|
||||
import {
|
||||
usePositionPresentationStore,
|
||||
useSegmentationPresentationStore,
|
||||
useSelectedSegmentationsForViewportStore,
|
||||
} from './stores';
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
|
||||
@ -43,6 +52,11 @@ import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import { isMeasurementWithinViewport } from './utils/isMeasurementWithinViewport';
|
||||
import { getCenterExtent } from './utils/getCenterExtent';
|
||||
import { EasingFunctionEnum } from './utils/transitions';
|
||||
import { createSegmentationForViewport } from './utils/createSegmentationForViewport';
|
||||
import { utilities as segmentationUtilities } from '@cornerstonejs/tools/segmentation';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
const { add, intersect, subtract, copy } = cstUtils.contourSegmentation;
|
||||
|
||||
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
|
||||
const toggleSyncFunctions = {
|
||||
@ -963,17 +977,21 @@ function commandsModule({
|
||||
}
|
||||
}
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [] }) => {
|
||||
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [], bindings }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
toolName = toolName || itemId || value;
|
||||
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
actions.setToolActive({ toolName, toolGroupId });
|
||||
actions.setToolActive({ toolName, toolGroupId, bindings });
|
||||
});
|
||||
},
|
||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||
setToolActive: ({
|
||||
toolName,
|
||||
toolGroupId = null,
|
||||
bindings = [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
}) => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
if (!viewports.size) {
|
||||
@ -1001,11 +1019,7 @@ function commandsModule({
|
||||
|
||||
// Set the new toolName to be active
|
||||
toolGroup.setToolActive(toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: Enums.MouseBindings.Primary,
|
||||
},
|
||||
],
|
||||
bindings,
|
||||
});
|
||||
},
|
||||
// capture viewport
|
||||
@ -1478,48 +1492,24 @@ function commandsModule({
|
||||
* as a segmentation representation to the viewport.
|
||||
*/
|
||||
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
|
||||
const { viewportGridService, displaySetService, segmentationService } =
|
||||
servicesManager.services;
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId;
|
||||
|
||||
const viewport = viewports.get(targetViewportId);
|
||||
|
||||
// Todo: add support for multiple display sets
|
||||
const displaySetInstanceUID =
|
||||
options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const segs = segmentationService.getSegmentations();
|
||||
|
||||
const label = options.label || `${i18n.t('Tools:Segmentation')} ${segs.length + 1}`;
|
||||
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// This will create the segmentation and register it as a display set
|
||||
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
|
||||
displaySet,
|
||||
{
|
||||
label,
|
||||
segmentationId,
|
||||
segments: options.createInitialSegment
|
||||
? {
|
||||
1: {
|
||||
label: `${i18n.t('Tools:Segment')} 1`,
|
||||
active: true,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
}
|
||||
);
|
||||
|
||||
// Also add the segmentation representation to the viewport
|
||||
await segmentationService.addSegmentationRepresentation(viewportId, {
|
||||
segmentationId,
|
||||
type: Enums.SegmentationRepresentations.Labelmap,
|
||||
return createSegmentationForViewport(servicesManager, {
|
||||
viewportId,
|
||||
options,
|
||||
segmentationType: SegmentationRepresentations.Labelmap,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Creates a contour for the active viewport
|
||||
*
|
||||
* The created contour will be registered as a display set and also added
|
||||
* as a segmentation representation to the viewport.
|
||||
*/
|
||||
createContourForViewport: async ({ viewportId, options = {} }) => {
|
||||
return createSegmentationForViewport(servicesManager, {
|
||||
viewportId,
|
||||
options,
|
||||
segmentationType: SegmentationRepresentations.Contour,
|
||||
});
|
||||
|
||||
return generatedSegmentationId;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1538,9 +1528,9 @@ function commandsModule({
|
||||
* Adds a new segment to a segmentation
|
||||
* @param props.segmentationId - The ID of the segmentation to add the segment to
|
||||
*/
|
||||
addSegmentCommand: ({ segmentationId }) => {
|
||||
addSegmentCommand: ({ segmentationId, config }) => {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
segmentationService.addSegment(segmentationId);
|
||||
segmentationService.addSegment(segmentationId, config);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -2269,6 +2259,207 @@ function commandsModule({
|
||||
deleting,
|
||||
});
|
||||
},
|
||||
activateSelectedSegmentationOfType: ({ segmentationRepresentationType }) => {
|
||||
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||
const activeViewportId = viewportGridService.getActiveViewportId();
|
||||
const { selectedSegmentationsForViewport } =
|
||||
useSelectedSegmentationsForViewportStore.getState();
|
||||
const segmentationId = selectedSegmentationsForViewport[activeViewportId]?.get(
|
||||
segmentationRepresentationType
|
||||
);
|
||||
|
||||
if (!segmentationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setActiveSegmentation(activeViewportId, segmentationId);
|
||||
},
|
||||
setDynamicCursorSizeForSculptorTool: ({ value: isDynamicCursorSize }) => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
const sculptorToolInstance = toolGroup.getToolInstance(toolNames.SculptorTool);
|
||||
const oldConfiguration = sculptorToolInstance.configuration;
|
||||
|
||||
sculptorToolInstance.configuration = {
|
||||
...oldConfiguration,
|
||||
updateCursorSize: isDynamicCursorSize ? 'dynamic' : '',
|
||||
};
|
||||
},
|
||||
setInterpolationToolConfiguration: ({ value: interpolateContours, toolNames }) => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
// Set the interpolation configuration for the active tool.
|
||||
const activeTool = toolGroupService.getActiveToolForViewport(viewportId);
|
||||
const interpolationConfig = {
|
||||
interpolation: {
|
||||
enabled: interpolateContours,
|
||||
showInterpolationPolyline: true,
|
||||
},
|
||||
};
|
||||
toolGroup.setToolConfiguration(activeTool, interpolationConfig);
|
||||
|
||||
// Now set the interpolation configuration for the other tools specified.
|
||||
if (toolNames) {
|
||||
Object.values(toolGroup.getToolInstances()).forEach(toolInstance => {
|
||||
if (toolNames?.includes(toolInstance.toolName)) {
|
||||
toolGroup.setToolConfiguration(toolInstance.toolName, interpolationConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
setSimplifiedSplineForSplineContourSegmentationTool: ({ value: simplifiedSpline }) => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
Object.values(toolGroup.getToolInstances()).forEach(toolInstance => {
|
||||
if (toolInstance instanceof SplineContourSegmentationTool) {
|
||||
const oldConfiguration = toolInstance.configuration;
|
||||
toolInstance.configuration = {
|
||||
...oldConfiguration,
|
||||
simplifiedSpline,
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
removeSmallContours: ({ areaThreshold: threshold }) => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
|
||||
const activeSegment = segmentationService.getActiveSegment(viewportId);
|
||||
|
||||
if (!activeSegmentation || !activeSegment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { removeContourIslands } = segmentationUtilities;
|
||||
removeContourIslands(activeSegmentation.segmentationId, activeSegment.segmentIndex, {
|
||||
threshold,
|
||||
});
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
applyLogicalContourOperation: ({
|
||||
segmentAInfo,
|
||||
segmentBInfo,
|
||||
resultSegmentInfo,
|
||||
logicalOperation,
|
||||
}: {
|
||||
segmentAInfo: SegmentInfo;
|
||||
segmentBInfo: SegmentInfo;
|
||||
resultSegmentInfo: OperatorOptions;
|
||||
logicalOperation: LogicalOperation;
|
||||
}) => {
|
||||
switch (logicalOperation) {
|
||||
case LogicalOperation.Union:
|
||||
add(segmentAInfo, segmentBInfo, resultSegmentInfo);
|
||||
break;
|
||||
case LogicalOperation.Intersect:
|
||||
intersect(segmentAInfo, segmentBInfo, resultSegmentInfo);
|
||||
break;
|
||||
case LogicalOperation.Subtract:
|
||||
subtract(segmentAInfo, segmentBInfo, resultSegmentInfo);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported logical operation');
|
||||
break;
|
||||
}
|
||||
},
|
||||
copyContourSegment: ({
|
||||
sourceSegmentInfo,
|
||||
targetSegmentInfo,
|
||||
}: {
|
||||
sourceSegmentInfo: SegmentInfo;
|
||||
targetSegmentInfo?: SegmentInfo;
|
||||
}) => {
|
||||
if (!targetSegmentInfo) {
|
||||
targetSegmentInfo = {
|
||||
segmentationId: sourceSegmentInfo.segmentationId,
|
||||
segmentIndex: segmentationService.getNextAvailableSegmentIndex(
|
||||
sourceSegmentInfo.segmentationId
|
||||
),
|
||||
};
|
||||
segmentationService.addSegment(targetSegmentInfo.segmentationId, {
|
||||
segmentIndex: targetSegmentInfo.segmentIndex,
|
||||
});
|
||||
}
|
||||
|
||||
copy(sourceSegmentInfo, targetSegmentInfo);
|
||||
},
|
||||
smoothContours: () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
|
||||
const activeSegment = segmentationService.getActiveSegment(viewportId);
|
||||
|
||||
if (!activeSegmentation || !activeSegment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { smoothContours } = segmentationUtilities;
|
||||
smoothContours(activeSegmentation.segmentationId, activeSegment.segmentIndex);
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
removeContourHoles: () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
|
||||
const activeSegment = segmentationService.getActiveSegment(viewportId);
|
||||
|
||||
if (!activeSegmentation || !activeSegment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { removeContourHoles } = segmentationUtilities;
|
||||
removeContourHoles(activeSegmentation.segmentationId, activeSegment.segmentIndex);
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
decimateContours: () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
|
||||
const activeSegment = segmentationService.getActiveSegment(viewportId);
|
||||
|
||||
if (!activeSegmentation || !activeSegment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { decimateContours } = segmentationUtilities;
|
||||
decimateContours(activeSegmentation.segmentationId, activeSegment.segmentIndex);
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
convertContourHoles: () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
|
||||
const activeSegment = segmentationService.getActiveSegment(viewportId);
|
||||
|
||||
if (!activeSegmentation || !activeSegment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSegmentInfo = {
|
||||
segmentationId: activeSegmentation.segmentationId,
|
||||
segmentIndex: segmentationService.getNextAvailableSegmentIndex(
|
||||
activeSegmentation.segmentationId
|
||||
),
|
||||
};
|
||||
|
||||
segmentationService.addSegment(targetSegmentInfo.segmentationId, {
|
||||
segmentIndex: targetSegmentInfo.segmentIndex,
|
||||
});
|
||||
|
||||
const { convertContourHoles } = segmentationUtilities;
|
||||
convertContourHoles(
|
||||
activeSegmentation.segmentationId,
|
||||
activeSegment.segmentIndex,
|
||||
targetSegmentInfo.segmentationId,
|
||||
targetSegmentInfo.segmentIndex
|
||||
);
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -2467,6 +2658,9 @@ function commandsModule({
|
||||
createLabelmapForViewport: {
|
||||
commandFn: actions.createLabelmapForViewport,
|
||||
},
|
||||
createContourForViewport: {
|
||||
commandFn: actions.createContourForViewport,
|
||||
},
|
||||
setActiveSegmentation: {
|
||||
commandFn: actions.setActiveSegmentation,
|
||||
},
|
||||
@ -2568,6 +2762,18 @@ function commandsModule({
|
||||
toggleSegmentLabel: actions.toggleSegmentLabel,
|
||||
jumpToMeasurementViewport: actions.jumpToMeasurementViewport,
|
||||
initializeSegmentLabelTool: actions.initializeSegmentLabelTool,
|
||||
activateSelectedSegmentationOfType: actions.activateSelectedSegmentationOfType,
|
||||
setDynamicCursorSizeForSculptorTool: actions.setDynamicCursorSizeForSculptorTool,
|
||||
setSimplifiedSplineForSplineContourSegmentationTool:
|
||||
actions.setSimplifiedSplineForSplineContourSegmentationTool,
|
||||
removeSmallContours: actions.removeSmallContours,
|
||||
applyLogicalContourOperation: actions.applyLogicalContourOperation,
|
||||
copyContourSegment: actions.copyContourSegment,
|
||||
smoothContours: actions.smoothContours,
|
||||
removeContourHoles: actions.removeContourHoles,
|
||||
decimateContours: actions.decimateContours,
|
||||
convertContourHoles: actions.convertContourHoles,
|
||||
setInterpolationToolConfiguration: actions.setInterpolationToolConfiguration,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
Icons,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
interface ExportSegmentationSubMenuItemProps {
|
||||
segmentationId: string;
|
||||
segmentationRepresentationType: string;
|
||||
allowExport: boolean;
|
||||
actions: {
|
||||
storeSegmentation: (segmentationId: string) => Promise<void>;
|
||||
onSegmentationDownloadRTSS: (segmentationId: string) => void;
|
||||
onSegmentationDownload: (segmentationId: string) => void;
|
||||
downloadCSVSegmentationReport: (segmentationId: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuItemProps> = ({
|
||||
segmentationId,
|
||||
segmentationRepresentationType,
|
||||
allowExport,
|
||||
actions,
|
||||
}) => {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
|
||||
return (
|
||||
<>
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="pl-1">
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Download & Export')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Download className="h-5 w-5" />
|
||||
<span className="pl-1">{t('Download')}</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.downloadCSVSegmentationReport(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('CSV Report')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownload(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownloadRTSS(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM RTSS')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Export className="h-5 w-5" />
|
||||
<span className="pl-1 pt-1">{t('Export')}</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.storeSegmentation(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Contour && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownloadRTSS(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Download DICOM RTSS')}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export default function SegmentationToolConfig() {
|
||||
const { commandsManager } = useSystem();
|
||||
|
||||
// Get initial states based on current configuration
|
||||
const [previewEdits, setPreviewEdits] = useState(false);
|
||||
const [segmentLabelEnabled, setSegmentLabelEnabled] = useState(false);
|
||||
const [toggleSegmentEnabled, setToggleSegmentEnabled] = useState(false);
|
||||
const [useCenterAsSegmentIndex, setUseCenterAsSegmentIndex] = useState(false);
|
||||
|
||||
const handlePreviewEditsChange = checked => {
|
||||
setPreviewEdits(checked);
|
||||
commandsManager.run('toggleSegmentPreviewEdit', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleToggleSegmentEnabledChange = checked => {
|
||||
setToggleSegmentEnabled(checked);
|
||||
commandsManager.run('toggleSegmentSelect', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleUseCenterAsSegmentIndexChange = checked => {
|
||||
setUseCenterAsSegmentIndex(checked);
|
||||
commandsManager.run('toggleUseCenterSegmentIndex', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleSegmentLabelEnabledChange = checked => {
|
||||
setSegmentLabelEnabled(checked);
|
||||
commandsManager.run('toggleSegmentLabel', { enabled: checked });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex flex-col gap-2 border-b border-b-[2px] border-black px-2 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={previewEdits}
|
||||
onCheckedChange={handlePreviewEditsChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Preview edits before creating</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={useCenterAsSegmentIndex}
|
||||
onCheckedChange={handleUseCenterAsSegmentIndexChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Use center as segment index</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={toggleSegmentEnabled}
|
||||
onCheckedChange={handleToggleSegmentEnabledChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Hover on segment border to activate</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={segmentLabelEnabled}
|
||||
onCheckedChange={handleSegmentLabelEnabledChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Show segment name on hover</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { cn, ToolButton } from '@ohif/ui-next';
|
||||
import { useUIStateStore } from '@ohif/extension-default';
|
||||
|
||||
interface SegmentationUtilityButtonProps {
|
||||
className?: string;
|
||||
isActive?: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that represents a segmentation utility.
|
||||
* It is implicitly the PopoverTrigger for the options Popover panel that is
|
||||
* opened in the PanelSegmentation component. It in essence is the PopoverTrigger
|
||||
* because typically this button has the associated options to display in the Popover.
|
||||
* Furthermore it also typically has a command for toggling the activeSegmentationUtility
|
||||
* value in the UIStateStore that triggers the fetching of its options.
|
||||
|
||||
* @param props - The props for the button.
|
||||
* @param props.className - The class name for the button.
|
||||
* @param props.isActive - Whether the button is active
|
||||
* @param props.id - The id of the button.
|
||||
*/
|
||||
function SegmentationUtilityButton(props: SegmentationUtilityButtonProps) {
|
||||
const { className, isActive, id } = props;
|
||||
|
||||
const activeSegmentationUtility = useUIStateStore(
|
||||
store => store.uiState.activeSegmentationUtility
|
||||
);
|
||||
|
||||
const toolButtonClassName = cn(
|
||||
'w-7 h-7 text-primary hover:text-primary hover:!bg-primary/30',
|
||||
className,
|
||||
isActive && 'bg-primary/30'
|
||||
);
|
||||
|
||||
const handleMouseDownCapture = useCallback(
|
||||
event => {
|
||||
if (activeSegmentationUtility === id) {
|
||||
// If this active button is clicked, prevent the default Popover
|
||||
// behaviour of closing the Popover on a pointer/mouse down.
|
||||
// Not doing this will cause the Popover to close and then reopen again.
|
||||
// Why? Because propagating this event will cause PanelSegmentation to
|
||||
// close the Popover by clearing the activeSegmentationUtility. Then
|
||||
// this button will set the activeSegmentationUtility again and the
|
||||
// Popover will reopen.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
[activeSegmentationUtility, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<div onPointerDownCapture={handleMouseDownCapture}>
|
||||
<ToolButton
|
||||
{...props}
|
||||
className={toolButtonClassName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SegmentationUtilityButton;
|
||||
@ -3,17 +3,14 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
Icons,
|
||||
useSegmentationTableContext,
|
||||
useSegmentationExpanded,
|
||||
} from '@ohif/ui-next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSystem } from '@ohif/core/src';
|
||||
import { ExportSegmentationSubMenuItem } from '../components/ExportSegmentationSubMenuItem';
|
||||
|
||||
/**
|
||||
* Custom dropdown menu component for segmentation panel that uses context for data
|
||||
@ -29,6 +26,8 @@ export const CustomDropdownMenuContent = () => {
|
||||
exportOptions,
|
||||
activeSegmentation,
|
||||
activeSegmentationId,
|
||||
segmentationRepresentationType,
|
||||
disableEditing,
|
||||
} = useSegmentationTableContext('CustomDropdownMenu');
|
||||
|
||||
// Try to get segmentation data from expanded context first, fall back to table context
|
||||
@ -78,10 +77,14 @@ export const CustomDropdownMenuContent = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => onSegmentationAdd(segmentationId)}>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">{t('Create New Segmentation')}</span>
|
||||
</DropdownMenuItem>
|
||||
{!disableEditing && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSegmentationAdd({ segmentationId, segmentationRepresentationType })}
|
||||
>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">{t('Create New Segmentation')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onSegmentationRemoveFromViewport(segmentationId)}>
|
||||
@ -92,61 +95,12 @@ export const CustomDropdownMenuContent = () => {
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">{t('Rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="pl-1">
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Download & Export')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Download className="h-5 w-5" />
|
||||
<span className="pl-1">{t('Download')}</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.downloadCSVSegmentationReport(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('CSV Report')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownload(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownloadRTSS(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM RTSS')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Export className="h-5 w-5" />
|
||||
<span className="pl-1 pt-1">{t('Export')}</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.storeSegmentation(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<ExportSegmentationSubMenuItem
|
||||
segmentationId={segmentationId}
|
||||
segmentationRepresentationType={segmentationRepresentationType}
|
||||
allowExport={allowExport}
|
||||
actions={actions}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onSegmentationDelete(segmentationId)}>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { CustomDropdownMenuContent } from './CustomDropdownMenuContent';
|
||||
import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader';
|
||||
import React, { useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
import SegmentationToolConfig from '../components/SegmentationToolConfig';
|
||||
import React from 'react';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
|
||||
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
|
||||
return {
|
||||
@ -9,10 +11,25 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
|
||||
'panelSegmentation.customSegmentStatisticsHeader': CustomSegmentStatisticsHeader,
|
||||
'panelSegmentation.disableEditing': false,
|
||||
'panelSegmentation.showAddSegment': true,
|
||||
'panelSegmentation.onSegmentationAdd': () => {
|
||||
'panelSegmentation.onSegmentationAdd': async ({
|
||||
segmentationRepresentationType = SegmentationRepresentations.Labelmap,
|
||||
}) => {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
const viewportId = viewportGridService.getState().activeViewportId;
|
||||
commandsManager.run('createLabelmapForViewport', { viewportId });
|
||||
if (segmentationRepresentationType === SegmentationRepresentations.Labelmap) {
|
||||
commandsManager.run('createLabelmapForViewport', { viewportId });
|
||||
} else if (segmentationRepresentationType === SegmentationRepresentations.Contour) {
|
||||
const segmentationId = await commandsManager.run('createContourForViewport', {
|
||||
viewportId,
|
||||
});
|
||||
cornerstoneTools.segmentation.config.style.setStyle(
|
||||
{ segmentationId, type: SegmentationRepresentations.Contour },
|
||||
{
|
||||
fillAlpha: 0.5,
|
||||
renderFill: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
'panelSegmentation.tableMode': 'collapsed',
|
||||
'panelSegmentation.readableText': {
|
||||
@ -33,67 +50,11 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
|
||||
lesionGlycolysis: 'Lesion Glycolysis',
|
||||
center: 'Center',
|
||||
},
|
||||
'segmentationToolbox.config': () => {
|
||||
// Get initial states based on current configuration
|
||||
const [previewEdits, setPreviewEdits] = useState(false);
|
||||
const [segmentLabelEnabled, setSegmentLabelEnabled] = useState(false);
|
||||
const [toggleSegmentEnabled, setToggleSegmentEnabled] = useState(false);
|
||||
const [useCenterAsSegmentIndex, setUseCenterAsSegmentIndex] = useState(false);
|
||||
const handlePreviewEditsChange = checked => {
|
||||
setPreviewEdits(checked);
|
||||
commandsManager.run('toggleSegmentPreviewEdit', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleToggleSegmentEnabledChange = checked => {
|
||||
setToggleSegmentEnabled(checked);
|
||||
commandsManager.run('toggleSegmentSelect', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleUseCenterAsSegmentIndexChange = checked => {
|
||||
setUseCenterAsSegmentIndex(checked);
|
||||
commandsManager.run('toggleUseCenterSegmentIndex', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleSegmentLabelEnabledChange = checked => {
|
||||
setSegmentLabelEnabled(checked);
|
||||
commandsManager.run('toggleSegmentLabel', { enabled: checked });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex flex-col gap-2 border-b border-b-[2px] border-black px-2 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={previewEdits}
|
||||
onCheckedChange={handlePreviewEditsChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Preview edits before creating</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={useCenterAsSegmentIndex}
|
||||
onCheckedChange={handleUseCenterAsSegmentIndexChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Use center as segment index</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={toggleSegmentEnabled}
|
||||
onCheckedChange={handleToggleSegmentEnabledChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Hover on segment border to activate</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={segmentLabelEnabled}
|
||||
onCheckedChange={handleSegmentLabelEnabledChange}
|
||||
/>
|
||||
<span className="text-foreground text-base">Show segment name on hover</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
'labelMapSegmentationToolbox.config': () => {
|
||||
return <SegmentationToolConfig />;
|
||||
},
|
||||
'contourSegmentationToolbox.config': () => {
|
||||
return <SegmentationToolConfig />;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,53 +1,67 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Toolbox } from '@ohif/extension-default';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
import PanelMeasurement from './panels/PanelMeasurement';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
|
||||
const wrappedPanelSegmentation = ({ configuration }) => {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
const toolSectionMap = {
|
||||
[SegmentationRepresentations.Labelmap]: toolbarService.sections.labelMapSegmentationToolbox,
|
||||
[SegmentationRepresentations.Contour]: toolbarService.sections.contourSegmentationToolbox,
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentation = props => {
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
...props?.configuration,
|
||||
}}
|
||||
segmentationRepresentationType={props?.segmentationRepresentationType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
|
||||
const wrappedPanelSegmentationNoHeader = props => {
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
...props?.configuration,
|
||||
}}
|
||||
segmentationRepresentationType={props?.segmentationRepresentationType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
const wrappedPanelSegmentationWithTools = props => {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
const tKey = `${props.segmentationRepresentationType ?? 'Segmentation'} tools`;
|
||||
const tValue = t(tKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
buttonSectionId={toolbarService.sections.segmentationToolbox}
|
||||
title="Segmentation Tools"
|
||||
buttonSectionId={toolSectionMap[props.segmentationRepresentationType]}
|
||||
title={tValue}
|
||||
/>
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
...props?.configuration,
|
||||
}}
|
||||
segmentationRepresentationType={props?.segmentationRepresentationType}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -82,11 +96,26 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
||||
component: wrappedPanelSegmentationNoHeader,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationWithTools',
|
||||
name: 'panelSegmentationWithToolsLabelMap',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationWithTools,
|
||||
label: i18n.t('SegmentationTable:Labelmap'),
|
||||
component: props =>
|
||||
wrappedPanelSegmentationWithTools({
|
||||
...props,
|
||||
segmentationRepresentationType: SegmentationRepresentations.Labelmap,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationWithToolsContour',
|
||||
iconName: 'tab-contours',
|
||||
iconLabel: 'Segmentation',
|
||||
label: i18n.t('SegmentationTable:Contour'),
|
||||
component: props =>
|
||||
wrappedPanelSegmentationWithTools({
|
||||
...props,
|
||||
segmentationRepresentationType: SegmentationRepresentations.Contour,
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@ -44,6 +44,7 @@ import {
|
||||
usePositionPresentationStore,
|
||||
useSegmentationPresentationStore,
|
||||
useSynchronizersStore,
|
||||
useSelectedSegmentationsForViewportStore,
|
||||
} from './stores';
|
||||
import { useToggleOneUpViewportGridStore } from '@ohif/extension-default';
|
||||
import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations';
|
||||
@ -109,6 +110,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
segmentationService.EVENTS.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
|
||||
@ -153,6 +155,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
useSynchronizersStore.getState().clearSynchronizersStore();
|
||||
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
|
||||
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
|
||||
useSelectedSegmentationsForViewportStore
|
||||
.getState()
|
||||
.clearSelectedSegmentationsForViewportStore();
|
||||
segmentationService.removeAllSegmentations();
|
||||
},
|
||||
|
||||
@ -256,6 +261,7 @@ export {
|
||||
usePositionPresentationStore,
|
||||
useSegmentationPresentationStore,
|
||||
useSynchronizersStore,
|
||||
useSelectedSegmentationsForViewportStore,
|
||||
Enums,
|
||||
useMeasurements,
|
||||
useActiveViewportSegmentationRepresentations,
|
||||
|
||||
@ -106,6 +106,12 @@ export default async function init({
|
||||
colorbarService.EVENTS.STATE_CHANGED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||
segmentationService.EVENTS.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
|
||||
]);
|
||||
|
||||
window.services = servicesManager.services;
|
||||
window.extensionManager = extensionManager;
|
||||
window.commandsManager = commandsManager;
|
||||
|
||||
@ -41,6 +41,10 @@ import {
|
||||
SegmentSelectTool,
|
||||
RegionSegmentPlusTool,
|
||||
SegmentLabelTool,
|
||||
LivewireContourSegmentationTool,
|
||||
SculptorTool,
|
||||
SplineContourSegmentationTool,
|
||||
LabelMapEditWithContourTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { LabelmapSlicePropagationTool, MarkerLabelmapTool } from '@cornerstonejs/ai';
|
||||
import * as polySeg from '@cornerstonejs/polymorphic-segmentation';
|
||||
@ -109,6 +113,10 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(LabelmapSlicePropagationTool);
|
||||
addTool(MarkerLabelmapTool);
|
||||
addTool(RegionSegmentPlusTool);
|
||||
addTool(LivewireContourSegmentationTool);
|
||||
addTool(SculptorTool);
|
||||
addTool(SplineContourSegmentationTool);
|
||||
addTool(LabelMapEditWithContourTool);
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
textBoxFontSize: '15px',
|
||||
@ -168,6 +176,10 @@ const toolNames = {
|
||||
LabelmapSlicePropagation: LabelmapSlicePropagationTool.toolName,
|
||||
MarkerLabelmap: MarkerLabelmapTool.toolName,
|
||||
RegionSegmentPlus: RegionSegmentPlusTool.toolName,
|
||||
LivewireContourSegmentation: LivewireContourSegmentationTool.toolName,
|
||||
SculptorTool: SculptorTool.toolName,
|
||||
SplineContourSegmentation: SplineContourSegmentationTool.toolName,
|
||||
LabelMapEditWithContourTool: LabelMapEditWithContourTool.toolName,
|
||||
};
|
||||
|
||||
export { toolNames };
|
||||
|
||||
@ -1,16 +1,92 @@
|
||||
import React from 'react';
|
||||
import { SegmentationTable } from '@ohif/ui-next';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
IconPresentationProvider,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
SegmentationTable,
|
||||
ToolSettings,
|
||||
} from '@ohif/ui-next';
|
||||
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
|
||||
import { metaData, cache } from '@cornerstonejs/core';
|
||||
import { useSystem } from '@ohif/core/src';
|
||||
import { useActiveToolOptions, useSystem } from '@ohif/core/src';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import { Toolbar, useUIStateStore } from '@ohif/extension-default';
|
||||
import SegmentationUtilityButton from '../components/SegmentationUtilityButton';
|
||||
import { useSelectedSegmentationsForViewportStore } from '../stores';
|
||||
import {
|
||||
hasExportableLabelMapData,
|
||||
hasExportableContourData,
|
||||
} from '../utils/segmentationExportUtils';
|
||||
|
||||
export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
type PanelSegmentationProps = {
|
||||
children?: React.ReactNode;
|
||||
|
||||
// The representation type for this segmentation panel. Undefined means all types.
|
||||
segmentationRepresentationType?: SegmentationRepresentations;
|
||||
} & withAppTypes;
|
||||
|
||||
export default function PanelSegmentation({
|
||||
children,
|
||||
segmentationRepresentationType,
|
||||
}: PanelSegmentationProps) {
|
||||
const { commandsManager, servicesManager } = useSystem();
|
||||
const { customizationService, displaySetService } = servicesManager.services;
|
||||
const {
|
||||
customizationService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolbarService,
|
||||
segmentationService,
|
||||
} = servicesManager.services;
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
|
||||
const utilitiesSectionMap = {
|
||||
[SegmentationRepresentations.Labelmap]: toolbarService.sections.labelMapSegmentationUtilities,
|
||||
[SegmentationRepresentations.Contour]: toolbarService.sections.contourSegmentationUtilities,
|
||||
};
|
||||
|
||||
const selectedSegmentationsForViewportMap = useSelectedSegmentationsForViewportStore(
|
||||
store => store.selectedSegmentationsForViewport[activeViewportId]
|
||||
);
|
||||
|
||||
const selectedSegmentationIdForType = segmentationRepresentationType
|
||||
? selectedSegmentationsForViewportMap?.get(segmentationRepresentationType)
|
||||
: segmentationService?.getActiveSegmentation(activeViewportId)?.segmentationId;
|
||||
|
||||
const buttonSection = utilitiesSectionMap[segmentationRepresentationType];
|
||||
|
||||
const { activeToolOptions: activeUtilityOptions } = useActiveToolOptions({
|
||||
buttonSectionId: buttonSection,
|
||||
});
|
||||
|
||||
const { segmentationsWithRepresentations, disabled } =
|
||||
useActiveViewportSegmentationRepresentations();
|
||||
|
||||
const setUIState = useUIStateStore(store => store.setUIState);
|
||||
|
||||
// useEffect for handling clicks on any of the non-active viewports.
|
||||
// The ViewportGrid stops the propagation of pointer/mouse events
|
||||
// for non-active viewports so the Popover below
|
||||
// is not closed when clicking on any of the non-active viewports.
|
||||
useEffect(() => {
|
||||
setUIState('activeSegmentationUtility', null);
|
||||
toolbarService.refreshToolbarState({ viewportId: activeViewportId });
|
||||
}, [activeViewportId, setUIState, toolbarService]);
|
||||
|
||||
// The callback for handling clicks outside of the Popover and, the SegmentationUtilityButton
|
||||
// that triggered it to open. Clicks outside those components must close the Popover.
|
||||
// The Popover is made visible whenever the options associated with the
|
||||
// activeSegmentationUtility exist. Thus clearing the activeSegmentationUtility
|
||||
// clears the associated options and will keep the Popover closed.
|
||||
const handlePopoverOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) {
|
||||
setUIState('activeSegmentationUtility', null);
|
||||
toolbarService.refreshToolbarState({ viewportId: activeViewportId });
|
||||
}
|
||||
},
|
||||
[activeViewportId, setUIState, toolbarService]
|
||||
);
|
||||
|
||||
// Extract customization options
|
||||
const segmentationTableMode = customizationService.getCustomization(
|
||||
'panelSegmentation.tableMode'
|
||||
@ -35,6 +111,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
},
|
||||
onSegmentAdd: segmentationId => {
|
||||
commandsManager.run('addSegment', { segmentationId });
|
||||
commandsManager.run('setActiveSegmentation', { segmentationId });
|
||||
},
|
||||
onSegmentClick: (segmentationId, segmentIndex) => {
|
||||
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
|
||||
@ -51,6 +128,14 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
onSegmentDelete: (segmentationId, segmentIndex) => {
|
||||
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
|
||||
},
|
||||
onSegmentCopy:
|
||||
segmentationRepresentationType === SegmentationRepresentations.Contour
|
||||
? (segmentationId, segmentIndex) => {
|
||||
commandsManager.run('copyContourSegment', {
|
||||
sourceSegmentInfo: { segmentationId, segmentIndex },
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
|
||||
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
|
||||
},
|
||||
@ -96,52 +181,26 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
};
|
||||
|
||||
// Generate export options
|
||||
// Map each segmentation to an export option for it.
|
||||
// A segmentation is exportable if it has any labelmap or contour data.
|
||||
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
|
||||
const { representationData, segmentationId } = segmentation;
|
||||
const { Labelmap } = representationData;
|
||||
const { Labelmap, Contour } = representationData;
|
||||
|
||||
if (!Labelmap) {
|
||||
if (!Labelmap && !Contour) {
|
||||
return { segmentationId, isExportable: true };
|
||||
}
|
||||
|
||||
// Check if any segments have anything drawn in any of the viewports
|
||||
const hasAnySegmentData = (() => {
|
||||
const imageIds = Labelmap.imageIds;
|
||||
if (!imageIds?.length) return false;
|
||||
|
||||
for (const imageId of imageIds) {
|
||||
const pixelData = cache.getImage(imageId)?.getPixelData();
|
||||
if (!pixelData) continue;
|
||||
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
if (pixelData[i] !== 0) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (!hasAnySegmentData) {
|
||||
if (
|
||||
!hasExportableLabelMapData(Labelmap, displaySetService) &&
|
||||
!hasExportableContourData(Contour)
|
||||
) {
|
||||
return { segmentationId, isExportable: false };
|
||||
}
|
||||
|
||||
const referencedImageIds = Labelmap.referencedImageIds;
|
||||
const firstImageId = referencedImageIds[0];
|
||||
const instance = metaData.get('instance', firstImageId);
|
||||
|
||||
if (!instance) {
|
||||
return { segmentationId, isExportable: false };
|
||||
}
|
||||
|
||||
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
|
||||
const SeriesInstanceUID = instance.SeriesInstanceUID;
|
||||
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
return {
|
||||
segmentationId,
|
||||
isExportable: displaySet?.isReconstructable,
|
||||
isExportable: true,
|
||||
};
|
||||
});
|
||||
|
||||
@ -150,15 +209,34 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
disabled,
|
||||
data: segmentationsWithRepresentations,
|
||||
mode: segmentationTableMode,
|
||||
title: 'Segmentations',
|
||||
title: `${segmentationRepresentationType ? `${segmentationRepresentationType} ` : ''}Segmentations`,
|
||||
exportOptions,
|
||||
disableEditing,
|
||||
onSegmentationAdd,
|
||||
showAddSegment,
|
||||
renderInactiveSegmentations: handlers.getRenderInactiveSegmentations(),
|
||||
segmentationRepresentationType,
|
||||
selectedSegmentationIdForType,
|
||||
...handlers,
|
||||
};
|
||||
|
||||
const renderUtilitiesToolbar = () => {
|
||||
if (!buttonSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconPresentationProvider
|
||||
size="large"
|
||||
IconContainer={SegmentationUtilityButton}
|
||||
>
|
||||
<div className="flex flex-wrap gap-[3px] bg-transparent pb-[2px] pl-[8px] pt-[6px]">
|
||||
<Toolbar buttonSection={buttonSection} />
|
||||
</div>
|
||||
</IconPresentationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSegments = () => {
|
||||
return (
|
||||
<SegmentationTable.Segments>
|
||||
@ -175,6 +253,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
if (tableProps.mode === 'collapsed') {
|
||||
return (
|
||||
<SegmentationTable.Collapsed>
|
||||
{renderUtilitiesToolbar()}
|
||||
<SegmentationTable.Collapsed.Header>
|
||||
<SegmentationTable.Collapsed.DropdownMenu>
|
||||
<CustomDropdownMenuContent />
|
||||
@ -193,6 +272,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
return (
|
||||
<>
|
||||
<SegmentationTable.Expanded>
|
||||
{renderUtilitiesToolbar()}
|
||||
<SegmentationTable.Expanded.Header>
|
||||
<SegmentationTable.Expanded.DropdownMenu>
|
||||
<CustomDropdownMenuContent />
|
||||
@ -211,11 +291,27 @@ export default function PanelSegmentation({ children }: withAppTypes) {
|
||||
};
|
||||
|
||||
return (
|
||||
<SegmentationTable {...tableProps}>
|
||||
{children}
|
||||
<SegmentationTable.Config />
|
||||
<SegmentationTable.AddSegmentationRow />
|
||||
{renderModeContent()}
|
||||
</SegmentationTable>
|
||||
<Popover
|
||||
open={!!activeUtilityOptions}
|
||||
onOpenChange={handlePopoverOpenChange}
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<SegmentationTable {...tableProps}>
|
||||
{children}
|
||||
<SegmentationTable.Config />
|
||||
<SegmentationTable.AddSegmentationRow />
|
||||
{renderModeContent()}
|
||||
</SegmentationTable>
|
||||
</PopoverAnchor>
|
||||
{activeUtilityOptions && (
|
||||
<PopoverContent
|
||||
side="left"
|
||||
align="start"
|
||||
className="w-auto"
|
||||
>
|
||||
<ToolSettings options={activeUtilityOptions} />
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@ -221,7 +221,7 @@ describe('SegmentationService', () => {
|
||||
it('should add event listeners', () => {
|
||||
service.onModeEnter();
|
||||
|
||||
expect(eventTarget.addEventListener).toHaveBeenCalledTimes(7);
|
||||
expect(eventTarget.addEventListener).toHaveBeenCalledTimes(8);
|
||||
|
||||
expect(eventTarget.addEventListener).toHaveBeenCalledWith(
|
||||
csToolsEnums.Events.SEGMENTATION_MODIFIED,
|
||||
@ -251,6 +251,10 @@ describe('SegmentationService', () => {
|
||||
csToolsEnums.Events.SEGMENTATION_ADDED,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(eventTarget.addEventListener).toHaveBeenCalledWith(
|
||||
csToolsEnums.Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1999,7 +2003,7 @@ describe('SegmentationService', () => {
|
||||
|
||||
service.addSegment(segmentationId, config);
|
||||
|
||||
expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1);
|
||||
expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(2);
|
||||
expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith(segmentationId);
|
||||
|
||||
expect(cstSegmentation.updateSegmentations).toHaveBeenCalledTimes(1);
|
||||
|
||||
@ -81,6 +81,8 @@ const EVENTS = {
|
||||
SEGMENT_LOADING_COMPLETE: 'event::segment_loading_complete',
|
||||
// loading completed for all segments
|
||||
SEGMENTATION_LOADING_COMPLETE: 'event::segmentation_loading_complete',
|
||||
// fired when a contour annotation cut merge process is completed
|
||||
ANNOTATION_CUT_MERGE_PROCESS_COMPLETED: 'event::annotation_cut_merge_process_completed',
|
||||
};
|
||||
|
||||
const VALUE_TYPES = {};
|
||||
@ -350,6 +352,39 @@ class SegmentationService extends PubSubService {
|
||||
FrameOfReferenceUID?: string;
|
||||
label?: string;
|
||||
}
|
||||
): Promise<string> {
|
||||
return this._createSegmentationForDisplaySet(displaySet, LABELMAP, options);
|
||||
}
|
||||
|
||||
public async createContourForDisplaySet(
|
||||
displaySet: AppTypes.DisplaySet,
|
||||
options?: {
|
||||
segmentationId?: string;
|
||||
segments?: { [segmentIndex: number]: Partial<cstTypes.Segment> };
|
||||
FrameOfReferenceUID?: string;
|
||||
label?: string;
|
||||
}
|
||||
): Promise<string> {
|
||||
return this._createSegmentationForDisplaySet(displaySet, CONTOUR, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to create segmentation for a display set with the specified type
|
||||
*
|
||||
* @param displaySet - The display set to create the segmentation for
|
||||
* @param segmentationType - The type of segmentation (SegmentationRepresentations enum)
|
||||
* @param options - Optional parameters for creating the segmentation
|
||||
* @returns A promise that resolves to the created segmentation ID
|
||||
*/
|
||||
private async _createSegmentationForDisplaySet(
|
||||
displaySet: AppTypes.DisplaySet,
|
||||
segmentationType: SegmentationRepresentations,
|
||||
options?: {
|
||||
segmentationId?: string;
|
||||
segments?: { [segmentIndex: number]: Partial<cstTypes.Segment> };
|
||||
FrameOfReferenceUID?: string;
|
||||
label?: string;
|
||||
}
|
||||
): Promise<string> {
|
||||
// Todo: random does not makes sense, make this better, like
|
||||
// labelmap 1, 2, 3 etc
|
||||
@ -375,7 +410,7 @@ class SegmentationService extends PubSubService {
|
||||
const segmentationPublicInput: cstTypes.SegmentationPublicInput = {
|
||||
segmentationId,
|
||||
representation: {
|
||||
type: LABELMAP,
|
||||
type: segmentationType,
|
||||
data: {
|
||||
imageIds: segImageIds,
|
||||
// referencedVolumeId: this._getVolumeIdForDisplaySet(displaySet),
|
||||
@ -808,6 +843,15 @@ class SegmentationService extends PubSubService {
|
||||
cstSegmentation.config.style.resetToGlobalStyle();
|
||||
};
|
||||
|
||||
public getNextAvailableSegmentIndex(segmentationId: string): number {
|
||||
const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
|
||||
// grab the next available segment index based on the object keys,
|
||||
// so basically get the highest segment index value + 1
|
||||
|
||||
const segmentKeys = Object.keys(csSegmentation.segments);
|
||||
return segmentKeys.length === 0 ? 1 : Math.max(...segmentKeys.map(Number)) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new segment to the specified segmentation.
|
||||
* @param segmentationId - The ID of the segmentation to add the segment to.
|
||||
@ -841,10 +885,7 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
let segmentIndex = config.segmentIndex;
|
||||
if (!segmentIndex) {
|
||||
// grab the next available segment index based on the object keys,
|
||||
// so basically get the highest segment index value + 1
|
||||
const segmentKeys = Object.keys(csSegmentation.segments);
|
||||
segmentIndex = segmentKeys.length === 0 ? 1 : Math.max(...segmentKeys.map(Number)) + 1;
|
||||
segmentIndex = this.getNextAvailableSegmentIndex(segmentationId);
|
||||
}
|
||||
|
||||
// update the segmentation
|
||||
@ -1627,6 +1668,11 @@ class SegmentationService extends PubSubService {
|
||||
csToolsEnums.Events.SEGMENTATION_ADDED,
|
||||
this._onSegmentationAddedFromSource
|
||||
);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
csToolsEnums.Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
|
||||
this._onAnnotationCutMergeProcessCompletedFromSource
|
||||
);
|
||||
}
|
||||
|
||||
private getCornerstoneSegmentation(segmentationId: string) {
|
||||
@ -1879,6 +1925,13 @@ class SegmentationService extends PubSubService {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
private _onAnnotationCutMergeProcessCompletedFromSource = evt => {
|
||||
const { segmentationId } = evt.detail;
|
||||
this._broadcastEvent(this.EVENTS.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SegmentationService;
|
||||
|
||||
@ -2,3 +2,4 @@ export { useLutPresentationStore } from './useLutPresentationStore';
|
||||
export { usePositionPresentationStore } from './usePositionPresentationStore';
|
||||
export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
|
||||
export { useSynchronizersStore } from './useSynchronizersStore';
|
||||
export { useSelectedSegmentationsForViewportStore } from './useSelectedSegmentationsForViewportStore';
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { SelectedSegmentationByTypeMap } from '../types/SelectedSegmentationByTypeMap';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'selectedSegmentationsForViewportId';
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the state and actions for managing selected segmentations for a viewport by representation type.
|
||||
*/
|
||||
type SelectedSegmentationsForViewportState = {
|
||||
/**
|
||||
* Stores a mapping from viewport id to a map of representation type to segmentation id.
|
||||
*/
|
||||
selectedSegmentationsForViewport: Record<string, SelectedSegmentationByTypeMap>;
|
||||
|
||||
/**
|
||||
* Sets the selected segmentations for a given viewport id.
|
||||
*
|
||||
* @param key - The viewport id.
|
||||
* @param value - The `SelectedSegmentationByTypeMap` to associate with the viewport id.
|
||||
*/
|
||||
setSelectedSegmentationsForViewport: (key: string, value: SelectedSegmentationByTypeMap) => void;
|
||||
|
||||
/**
|
||||
* Clears all selected segmentations for all viewports.
|
||||
*/
|
||||
clearSelectedSegmentationsForViewportStore: () => void;
|
||||
|
||||
/**
|
||||
* Type identifier for the store.
|
||||
*/
|
||||
type: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the Selected Segmentations For Viewport store.
|
||||
*
|
||||
* @param set - The zustand set function.
|
||||
* @returns The selected segmentations for viewport store state and actions.
|
||||
*/
|
||||
const createSelectedSegmentationsForViewportStore = (
|
||||
set
|
||||
): SelectedSegmentationsForViewportState => ({
|
||||
selectedSegmentationsForViewport: {},
|
||||
type: PRESENTATION_TYPE_ID,
|
||||
|
||||
/**
|
||||
* Sets the selected segmentations for a given viewport id.
|
||||
*/
|
||||
setSelectedSegmentationsForViewport: (key, value) =>
|
||||
set(
|
||||
state => ({
|
||||
selectedSegmentationsForViewport: {
|
||||
...state.selectedSegmentationsForViewport,
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
false,
|
||||
'setSelectedSegmentationsForViewport'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clears all selected segmentations for all viewports.
|
||||
*/
|
||||
clearSelectedSegmentationsForViewportStore: () =>
|
||||
set(
|
||||
{ selectedSegmentationsForViewport: {} },
|
||||
false,
|
||||
'clearSelectedSegmentationsForViewportStore'
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zustand store for managing selected segmentations for a viewport by representation type.
|
||||
* Applies devtools middleware when DEBUG_STORE is enabled.
|
||||
*/
|
||||
export const useSelectedSegmentationsForViewportStore =
|
||||
create<SelectedSegmentationsForViewportState>()(
|
||||
DEBUG_STORE
|
||||
? devtools(createSelectedSegmentationsForViewportStore, {
|
||||
name: 'SelectedSegmentationsForViewportStore',
|
||||
})
|
||||
: createSelectedSegmentationsForViewportStore
|
||||
);
|
||||
@ -0,0 +1,3 @@
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
export type SelectedSegmentationByTypeMap = Map<SegmentationRepresentations, string>;
|
||||
@ -0,0 +1,74 @@
|
||||
import { utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
function _createDefaultSegments(createInitialSegment?: boolean) {
|
||||
return createInitialSegment
|
||||
? {
|
||||
1: {
|
||||
label: `${i18n.t('Tools:Segment')} 1`,
|
||||
active: true,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
type CreateSegmentationForViewportOptions = {
|
||||
displaySetInstanceUID?: string;
|
||||
label?: string;
|
||||
segmentationId?: string;
|
||||
createInitialSegment?: boolean;
|
||||
};
|
||||
|
||||
type CreateSegmentationForViewportParams = {
|
||||
viewportId: string;
|
||||
options?: CreateSegmentationForViewportOptions;
|
||||
segmentationType: SegmentationRepresentations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a segmentation for the active viewport
|
||||
*
|
||||
* The created segmentation will be registered as a display set and also added
|
||||
* as a segmentation representation to the viewport.
|
||||
*/
|
||||
export async function createSegmentationForViewport(
|
||||
servicesManager: ServicesManager,
|
||||
{ viewportId, options = {}, segmentationType }: CreateSegmentationForViewportParams
|
||||
): Promise<string> {
|
||||
const { viewportGridService, displaySetService, segmentationService } = servicesManager.services;
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId;
|
||||
|
||||
const viewport = viewports.get(targetViewportId);
|
||||
|
||||
// Todo: add support for multiple display sets
|
||||
const displaySetInstanceUID = options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const segs = segmentationService.getSegmentations();
|
||||
|
||||
const label = options.label || `${i18n.t('Tools:Segmentation')} ${segs.length + 1}`;
|
||||
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
const segmentationCreationOptions = {
|
||||
label,
|
||||
segmentationId,
|
||||
segments: _createDefaultSegments(options.createInitialSegment),
|
||||
};
|
||||
|
||||
// This will create the segmentation and register it as a display set
|
||||
const generatedSegmentationId = await (segmentationType === SegmentationRepresentations.Labelmap
|
||||
? segmentationService.createLabelmapForDisplaySet(displaySet, segmentationCreationOptions)
|
||||
: segmentationService.createContourForDisplaySet(displaySet, segmentationCreationOptions));
|
||||
|
||||
// Also add the segmentation representation to the viewport
|
||||
await segmentationService.addSegmentationRepresentation(viewportId, {
|
||||
segmentationId,
|
||||
type: segmentationType,
|
||||
});
|
||||
|
||||
return generatedSegmentationId;
|
||||
}
|
||||
@ -10,6 +10,7 @@ import promptHydrationDialog, {
|
||||
HydrationSRResult,
|
||||
} from './promptHydrationDialog';
|
||||
import { getCenterExtent } from './getCenterExtent';
|
||||
import { createSegmentationForViewport } from './createSegmentationForViewport';
|
||||
|
||||
const utils = {
|
||||
handleSegmentChange,
|
||||
@ -18,6 +19,7 @@ const utils = {
|
||||
setupSegmentationModifiedHandler,
|
||||
promptHydrationDialog,
|
||||
getCenterExtent,
|
||||
createSegmentationForViewport,
|
||||
};
|
||||
|
||||
export type { HydrationDialogProps, HydrationCallback, HydrationSRResult };
|
||||
|
||||
86
extensions/cornerstone/src/utils/segmentationExportUtils.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import { Types as cstTypes } from '@cornerstonejs/tools';
|
||||
|
||||
/**
|
||||
* Checks if a labelmap segmentation has exportable data
|
||||
* A label map is exportable if it has any pixel data and
|
||||
* it is referenced by a display set that is reconstructable.
|
||||
*
|
||||
* @param labelmap - The labelmap representation data
|
||||
* @param displaySetService - The display set service instance
|
||||
* @returns boolean - Whether the labelmap has exportable data
|
||||
*/
|
||||
export const hasExportableLabelMapData = (
|
||||
labelmap: cstTypes.LabelmapSegmentationData | undefined,
|
||||
displaySetService: any
|
||||
): boolean => {
|
||||
if (!labelmap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const imageIds = labelmap?.imageIds;
|
||||
if (!imageIds?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasPixelData = imageIds.some(imageId => {
|
||||
const pixelData = cache.getImage(imageId)?.getPixelData();
|
||||
if (!pixelData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
if (pixelData[i] !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasPixelData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const referencedImageIds = labelmap?.referencedImageIds;
|
||||
|
||||
if (!referencedImageIds) {
|
||||
return false;
|
||||
}
|
||||
const firstImageId = referencedImageIds[0];
|
||||
const instance = metaData.get('instance', firstImageId);
|
||||
|
||||
if (!instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
|
||||
const SeriesInstanceUID = instance.SeriesInstanceUID;
|
||||
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
return displaySet?.isReconstructable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a contour segmentation has exportable data
|
||||
* A contour is exportable if it has any contour annotation/geometry data.
|
||||
*
|
||||
* @param contour - The contour representation data
|
||||
* @returns boolean - Whether the contour has exportable data
|
||||
*/
|
||||
export const hasExportableContourData = (
|
||||
contour: cstTypes.ContourSegmentationData | undefined
|
||||
): boolean => {
|
||||
if (!contour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contourAnnotationUIDsMap = contour?.annotationUIDsMap;
|
||||
|
||||
if (!contourAnnotationUIDsMap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return contourAnnotationUIDsMap.size > 0;
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import { updateSegmentationStats } from './updateSegmentationStats';
|
||||
import { useSelectedSegmentationsForViewportStore } from '../stores';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
/**
|
||||
* Sets up the handler for segmentation data modification events
|
||||
@ -110,3 +112,49 @@ export function setupSegmentationModifiedHandler({ segmentationService }) {
|
||||
|
||||
return { unsubscribe };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up auto tab switching for when the first segmentation is added into the viewer.
|
||||
*/
|
||||
export function setUpSelectedSegmentationsForViewportHandler({ segmentationService }) {
|
||||
const selectedSegmentationsForViewportEvents = [
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||
];
|
||||
|
||||
const unsubscribeSelectedSegmentationsForViewportEvents = selectedSegmentationsForViewportEvents
|
||||
.map(eventName =>
|
||||
segmentationService.subscribe(eventName, event => {
|
||||
const { viewportId } = event;
|
||||
|
||||
if (!viewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { selectedSegmentationsForViewport, setSelectedSegmentationsForViewport } =
|
||||
useSelectedSegmentationsForViewportStore.getState();
|
||||
|
||||
const representations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
|
||||
const activeRepresentation = representations.find(representation => representation.active);
|
||||
|
||||
const typeToSegmentationIdMap =
|
||||
selectedSegmentationsForViewport[viewportId] ??
|
||||
new Map<SegmentationRepresentations, string>();
|
||||
|
||||
if (activeRepresentation) {
|
||||
typeToSegmentationIdMap.set(
|
||||
activeRepresentation.type,
|
||||
activeRepresentation.segmentationId
|
||||
);
|
||||
} else {
|
||||
typeToSegmentationIdMap.clear();
|
||||
}
|
||||
|
||||
setSelectedSegmentationsForViewport(viewportId, typeToSegmentationIdMap);
|
||||
})
|
||||
)
|
||||
.map(subscription => subscription.unsubscribe);
|
||||
|
||||
return { unsubscribeSelectedSegmentationsForViewportEvents };
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@ import { setUpSegmentationEventHandlers } from './setUpSegmentationEventHandlers
|
||||
import {
|
||||
setupSegmentationDataModifiedHandler,
|
||||
setupSegmentationModifiedHandler,
|
||||
setUpSelectedSegmentationsForViewportHandler,
|
||||
} from './segmentationHandlers';
|
||||
|
||||
jest.mock('./segmentationHandlers', () => ({
|
||||
setupSegmentationDataModifiedHandler: jest.fn(),
|
||||
setupSegmentationModifiedHandler: jest.fn(),
|
||||
setUpSelectedSegmentationsForViewportHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('setUpSegmentationEventHandlers', () => {
|
||||
@ -38,6 +40,7 @@ describe('setUpSegmentationEventHandlers', () => {
|
||||
const mockUnsubscribeDataModified = jest.fn();
|
||||
const mockUnsubscribeModified = jest.fn();
|
||||
const mockUnsubscribeCreated = jest.fn();
|
||||
const mockUnsubscribeSelectedSegmentationsForViewportEvents = [jest.fn(), jest.fn()];
|
||||
|
||||
const defaultParameters = {
|
||||
servicesManager: mockServicesManager as unknown as AppTypes.ServicesManager,
|
||||
@ -52,6 +55,10 @@ describe('setUpSegmentationEventHandlers', () => {
|
||||
(setupSegmentationModifiedHandler as jest.Mock).mockReturnValue({
|
||||
unsubscribe: mockUnsubscribeModified,
|
||||
});
|
||||
(setUpSelectedSegmentationsForViewportHandler as jest.Mock).mockReturnValue({
|
||||
unsubscribeSelectedSegmentationsForViewportEvents:
|
||||
mockUnsubscribeSelectedSegmentationsForViewportEvents,
|
||||
});
|
||||
mockSegmentationService.subscribe.mockReturnValue({
|
||||
unsubscribe: mockUnsubscribeCreated,
|
||||
});
|
||||
@ -92,6 +99,7 @@ describe('setUpSegmentationEventHandlers', () => {
|
||||
mockUnsubscribeDataModified,
|
||||
mockUnsubscribeModified,
|
||||
mockUnsubscribeCreated,
|
||||
...mockUnsubscribeSelectedSegmentationsForViewportEvents,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
setUpSelectedSegmentationsForViewportHandler,
|
||||
setupSegmentationDataModifiedHandler,
|
||||
setupSegmentationModifiedHandler,
|
||||
} from './segmentationHandlers';
|
||||
@ -28,7 +29,9 @@ export const setUpSegmentationEventHandlers = ({ servicesManager, commandsManage
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const label = segmentation.cachedStats.info;
|
||||
const imageIds = segmentation.representationData.Labelmap.imageIds;
|
||||
const imageIds =
|
||||
segmentation.representationData?.Labelmap?.imageIds ??
|
||||
segmentation.representationData?.Contour?.imageIds;
|
||||
|
||||
// Create a display set for the segmentation
|
||||
const segmentationDisplaySet = {
|
||||
@ -50,10 +53,16 @@ export const setUpSegmentationEventHandlers = ({ servicesManager, commandsManage
|
||||
}
|
||||
);
|
||||
|
||||
const { unsubscribeSelectedSegmentationsForViewportEvents } =
|
||||
setUpSelectedSegmentationsForViewportHandler({
|
||||
segmentationService,
|
||||
});
|
||||
|
||||
const unsubscriptions = [
|
||||
unsubscribeSegmentationDataModifiedHandler,
|
||||
unsubscribeSegmentationModifiedHandler,
|
||||
unsubscribeSegmentationCreated,
|
||||
...unsubscribeSelectedSegmentationsForViewportEvents,
|
||||
];
|
||||
|
||||
return { unsubscriptions };
|
||||
|
||||
@ -28,7 +28,7 @@ const SidePanelWithServices = ({
|
||||
onClose,
|
||||
...props
|
||||
}: SidePanelWithServicesProps) => {
|
||||
const panelService = servicesManager?.services?.panelService;
|
||||
const { panelService, toolbarService, viewportGridService } = servicesManager.services;
|
||||
|
||||
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
||||
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||
@ -37,9 +37,15 @@ const SidePanelWithServices = ({
|
||||
const [closedManually, setClosedManually] = useState(false);
|
||||
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
|
||||
|
||||
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
}, []);
|
||||
const handleActiveTabIndexChange = useCallback(
|
||||
({ activeTabIndex }) => {
|
||||
const { activeViewportId: viewportId } = viewportGridService.getState();
|
||||
toolbarService.refreshToolbarState({ viewportId });
|
||||
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
},
|
||||
[toolbarService, viewportGridService]
|
||||
);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setSidePanelExpanded(true);
|
||||
|
||||
@ -1,9 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Props for the Toolbar component that renders a collection of toolbar buttons and/or button sections.
|
||||
*
|
||||
* @interface ToolbarProps
|
||||
*/
|
||||
interface ToolbarProps {
|
||||
/**
|
||||
* The section of buttons to display in the toolbar.
|
||||
* Common values include 'primary', 'secondary', 'tertiary', etc.
|
||||
* Defaults to 'primary' if not specified.
|
||||
*
|
||||
* @default 'primary'
|
||||
*/
|
||||
buttonSection?: string;
|
||||
|
||||
/**
|
||||
* The unique identifier of the viewport this toolbar is associated with.
|
||||
*/
|
||||
viewportId?: string;
|
||||
|
||||
/**
|
||||
* The numeric position or location of the toolbar.
|
||||
* Used for ordering and layout purposes in the UI.
|
||||
*/
|
||||
location?: number;
|
||||
}
|
||||
|
||||
@ -60,7 +81,17 @@ export function Toolbar({ buttonSection = 'primary', viewportId, location }: Too
|
||||
/>
|
||||
);
|
||||
|
||||
return <div key={id}>{tool}</div>;
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
// This wrapper div exists solely for React's key prop requirement during reconciliation.
|
||||
// We use display:contents to make it transparent to the layout engine (children appear
|
||||
// as direct children of the parent) while keeping it in the DOM for React's virtual DOM.
|
||||
className="contents"
|
||||
>
|
||||
{tool}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -10,6 +10,7 @@ import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper';
|
||||
import ToolRowWrapper from './Toolbar/ToolRowWrapper';
|
||||
import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper';
|
||||
import { ToolButtonWrapper } from './Toolbar/ToolButtonWrapper';
|
||||
import { Toolbar } from './Toolbar';
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
|
||||
const { cineService } = servicesManager.services;
|
||||
@ -45,6 +46,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
name: 'ohif.progressDropdown',
|
||||
defaultComponent: ProgressDropdownWithService,
|
||||
},
|
||||
{
|
||||
name: 'ohif.Toolbar',
|
||||
defaultComponent: Toolbar,
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cine',
|
||||
evaluate: () => {
|
||||
|
||||
@ -1,12 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icons, PanelSection, ToolSettings } from '@ohif/ui-next';
|
||||
import { useSystem, useToolbar } from '@ohif/core';
|
||||
import classnames from 'classnames';
|
||||
import { useSystem, useToolbar, useActiveToolOptions } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ButtonProps {
|
||||
isActive?: boolean;
|
||||
options?: unknown;
|
||||
/**
|
||||
* Props for the Toolbox component that renders a collection of toolbar button sections.
|
||||
*/
|
||||
interface ToolboxProps {
|
||||
/**
|
||||
* The unique identifier of the button section this toolbox represents.
|
||||
*/
|
||||
buttonSectionId: string;
|
||||
|
||||
/**
|
||||
* The display title for the toolbox.
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -19,7 +28,7 @@ interface ButtonProps {
|
||||
* role in enhancing the app with a toolbox by providing a way to integrate
|
||||
* and display various tools and their corresponding options
|
||||
*/
|
||||
export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; title: string }) {
|
||||
export function Toolbox({ buttonSectionId, title }: ToolboxProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -30,6 +39,8 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
|
||||
buttonSection: buttonSectionId,
|
||||
});
|
||||
|
||||
const { activeToolOptions } = useActiveToolOptions({ buttonSectionId });
|
||||
|
||||
if (!toolboxSections.length) {
|
||||
return null;
|
||||
}
|
||||
@ -41,35 +52,6 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to check a list of buttons for an active tool.
|
||||
const findActiveOptions = (buttons: any[]): unknown => {
|
||||
for (const tool of buttons) {
|
||||
if (tool.componentProps.isActive) {
|
||||
return tool.componentProps.options;
|
||||
}
|
||||
if (tool.componentProps.buttonSection) {
|
||||
const nestedButtons = toolbarService.getButtonPropsInButtonSection(
|
||||
tool.componentProps.buttonSection
|
||||
) as ButtonProps[];
|
||||
const activeNested = nestedButtons.find(nested => nested.isActive);
|
||||
if (activeNested) {
|
||||
return activeNested.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Look for active tool options across all sections.
|
||||
const activeToolOptions = toolboxSections.reduce((activeOptions, section) => {
|
||||
if (activeOptions) {
|
||||
return activeOptions;
|
||||
}
|
||||
const sectionId = section.componentProps.buttonSection;
|
||||
const buttons = toolbarService.getButtonSection(sectionId);
|
||||
return findActiveOptions(buttons);
|
||||
}, null);
|
||||
|
||||
// Define the interaction handler once.
|
||||
const handleInteraction = ({ itemId }: { itemId: string }) => {
|
||||
onInteraction?.({ itemId });
|
||||
@ -103,19 +85,20 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
|
||||
return (
|
||||
<div
|
||||
key={sectionId}
|
||||
className="bg-muted flex flex-wrap space-x-2 py-2 px-1"
|
||||
className="bg-muted flex flex-wrap gap-2 py-2 px-1"
|
||||
>
|
||||
{buttons.map(tool => {
|
||||
if (!tool) {
|
||||
// Skip over tools that are not visible. The visible flag is typically set to
|
||||
// false as a result of the evaluator function. The evaluator might explicitly
|
||||
// set visible to false. Alternatively, the ToolbarService will set the visible flag to
|
||||
// false when the evaluator sets disabled to true and the tool has the hideWhenDisabled flag set to true.
|
||||
if (!tool || !tool.componentProps.visible) {
|
||||
return null;
|
||||
}
|
||||
const { id, Component, componentProps } = tool;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={classnames('ml-1')}
|
||||
>
|
||||
<div key={id}>
|
||||
<Component
|
||||
{...componentProps}
|
||||
id={id}
|
||||
|
||||
@ -659,14 +659,6 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
// Section containers for the nested toolbox
|
||||
{
|
||||
id: 'SegmentationUtilities',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SegmentLabelTool',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
|
||||
@ -71,11 +71,11 @@ const config = {
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false
|
||||
removeViewBox: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
prettier: false,
|
||||
svgo: true,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { id } from './id';
|
||||
import toolbarButtons from './toolbarButtons';
|
||||
import initToolGroups from './initToolGroups';
|
||||
import setUpAutoTabSwitchHandler from './utils/setUpAutoTabSwitchHandler';
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
@ -11,7 +12,10 @@ const ohif = {
|
||||
|
||||
const cornerstone = {
|
||||
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
|
||||
panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools',
|
||||
labelMapSegmentationPanel:
|
||||
'@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap',
|
||||
contourSegmentationPanel:
|
||||
'@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour',
|
||||
measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
|
||||
};
|
||||
|
||||
@ -36,6 +40,7 @@ const extensionDependencies = {
|
||||
};
|
||||
|
||||
function modeFactory({ modeConfiguration }) {
|
||||
const _unsubscriptions = [];
|
||||
return {
|
||||
/**
|
||||
* Mode ID, which should be unique among modes used by the viewer. This ID
|
||||
@ -53,8 +58,14 @@ function modeFactory({ modeConfiguration }) {
|
||||
* Services and other resources.
|
||||
*/
|
||||
onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
|
||||
const { measurementService, toolbarService, toolGroupService, customizationService } =
|
||||
servicesManager.services;
|
||||
const {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
panelService,
|
||||
} = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
@ -114,22 +125,54 @@ function modeFactory({ modeConfiguration }) {
|
||||
'TagBrowser',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection(toolbarService.sections.segmentationToolbox, [
|
||||
'SegmentationUtilities',
|
||||
'SegmentationTools',
|
||||
toolbarService.updateSection(toolbarService.sections.labelMapSegmentationToolbox, [
|
||||
'LabelMapTools',
|
||||
]);
|
||||
toolbarService.updateSection('SegmentationUtilities', [
|
||||
toolbarService.updateSection(toolbarService.sections.contourSegmentationToolbox, [
|
||||
'ContourTools',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection('LabelMapTools', [
|
||||
'LabelmapSlicePropagation',
|
||||
'InterpolateLabelmap',
|
||||
'SegmentBidirectional',
|
||||
]);
|
||||
toolbarService.updateSection('SegmentationTools', [
|
||||
'BrushTools',
|
||||
'MarkerLabelmap',
|
||||
'RegionSegmentPlus',
|
||||
'Shapes',
|
||||
'LabelMapEditWithContour',
|
||||
]);
|
||||
toolbarService.updateSection('ContourTools', [
|
||||
'PlanarFreehandContourSegmentationTool',
|
||||
'SculptorTool',
|
||||
'SplineContourSegmentationTool',
|
||||
'LivewireContourSegmentationTool',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection(toolbarService.sections.labelMapSegmentationUtilities, [
|
||||
'LabelMapUtilities',
|
||||
]);
|
||||
toolbarService.updateSection(toolbarService.sections.contourSegmentationUtilities, [
|
||||
'ContourUtilities',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection('LabelMapUtilities', [
|
||||
'InterpolateLabelmap',
|
||||
'SegmentBidirectional',
|
||||
]);
|
||||
toolbarService.updateSection('ContourUtilities', [
|
||||
'LogicalContourOperations',
|
||||
'SimplifyContours',
|
||||
'SmoothContours',
|
||||
]);
|
||||
|
||||
toolbarService.updateSection('BrushTools', ['Brush', 'Eraser', 'Threshold']);
|
||||
|
||||
const { unsubscribeAutoTabSwitchEvents } = setUpAutoTabSwitchHandler({
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
panelService,
|
||||
});
|
||||
|
||||
_unsubscriptions.push(...unsubscribeAutoTabSwitchEvents);
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const {
|
||||
@ -141,6 +184,9 @@ function modeFactory({ modeConfiguration }) {
|
||||
uiModalService,
|
||||
} = servicesManager.services;
|
||||
|
||||
_unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
_unsubscriptions.length = 0;
|
||||
|
||||
uiDialogService.hideAll();
|
||||
uiModalService.hide();
|
||||
toolGroupService.destroy();
|
||||
@ -192,7 +238,10 @@ function modeFactory({ modeConfiguration }) {
|
||||
props: {
|
||||
leftPanels: [ohif.leftPanel],
|
||||
leftPanelResizable: true,
|
||||
rightPanels: [cornerstone.panelTool],
|
||||
rightPanels: [
|
||||
cornerstone.contourSegmentationPanel,
|
||||
cornerstone.labelMapSegmentationPanel,
|
||||
],
|
||||
rightPanelResizable: true,
|
||||
// leftPanelClosed: true,
|
||||
viewports: [
|
||||
|
||||
@ -106,6 +106,9 @@ function createTools({ utilityModule, commandsManager }) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: toolNames.LabelMapEditWithContourTool,
|
||||
},
|
||||
{ toolName: toolNames.CircleScissors },
|
||||
{ toolName: toolNames.RectangleScissors },
|
||||
{ toolName: toolNames.SphereScissors },
|
||||
@ -114,6 +117,42 @@ function createTools({ utilityModule, commandsManager }) {
|
||||
{ toolName: toolNames.WindowLevelRegion },
|
||||
|
||||
{ toolName: toolNames.UltrasoundDirectional },
|
||||
{
|
||||
toolName: toolNames.PlanarFreehandContourSegmentation,
|
||||
},
|
||||
{ toolName: toolNames.LivewireContourSegmentation },
|
||||
{ toolName: toolNames.SculptorTool },
|
||||
{ toolName: toolNames.PlanarFreehandROI },
|
||||
{
|
||||
toolName: 'CatmullRomSplineROI',
|
||||
parentTool: toolNames.SplineContourSegmentation,
|
||||
configuration: {
|
||||
spline: {
|
||||
type: 'CATMULLROM',
|
||||
enableTwoPointPreview: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'LinearSplineROI',
|
||||
parentTool: toolNames.SplineContourSegmentation,
|
||||
configuration: {
|
||||
spline: {
|
||||
type: 'LINEAR',
|
||||
enableTwoPointPreview: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
toolName: 'BSplineROI',
|
||||
parentTool: toolNames.SplineContourSegmentation,
|
||||
configuration: {
|
||||
spline: {
|
||||
type: 'BSPLINE',
|
||||
enableTwoPointPreview: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }, { toolName: toolNames.AdvancedMagnify }],
|
||||
};
|
||||
|
||||
@ -174,17 +174,31 @@ const toolbarButtons: Button[] = [
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
// Section containers for the nested toolbox
|
||||
// Section containers for the nested toolboxes and toolbars.
|
||||
{
|
||||
id: 'SegmentationUtilities',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
id: 'LabelMapUtilities',
|
||||
uiType: 'ohif.Toolbar',
|
||||
props: {
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SegmentationTools',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
id: 'ContourUtilities',
|
||||
uiType: 'ohif.Toolbar',
|
||||
props: {
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LabelMapTools',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ContourTools',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
buttonSection: true,
|
||||
},
|
||||
@ -399,17 +413,254 @@ const toolbarButtons: Button[] = [
|
||||
commands: 'openDICOMTagViewer',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'PlanarFreehandContourSegmentationTool',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'icon-tool-freehand-roi',
|
||||
label: 'Freehand Segmentation',
|
||||
tooltip: 'Freehand Segmentation',
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['PlanarFreehandContourSegmentationTool'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: 1, // Left Click
|
||||
},
|
||||
{
|
||||
mouseButton: 1, // Left Click+Shift to create a hole
|
||||
modifierKey: 16, // Shift
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Interpolate Contours',
|
||||
type: 'switch',
|
||||
id: 'planarFreehandInterpolateContours',
|
||||
value: false,
|
||||
commands: {
|
||||
commandName: 'setInterpolationToolConfiguration',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LivewireContourSegmentationTool',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'icon-tool-livewire',
|
||||
label: 'Livewire Contour',
|
||||
tooltip: 'Livewire Contour',
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['LivewireContourSegmentationTool'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: 1, // Left Click
|
||||
},
|
||||
{
|
||||
mouseButton: 1, // Left Click+Shift to create a hole
|
||||
modifierKey: 16, // Shift
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Interpolate Contours',
|
||||
type: 'switch',
|
||||
id: 'livewireInterpolateContours',
|
||||
value: false,
|
||||
commands: {
|
||||
commandName: 'setInterpolationToolConfiguration',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SplineContourSegmentationTool',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'icon-tool-spline-roi',
|
||||
label: 'Spline Contour Segmentation Tool',
|
||||
tooltip: 'Spline Contour Segmentation Tool',
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CatmullRomSplineROI', 'LinearSplineROI', 'BSplineROI'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Spline Type',
|
||||
type: 'select',
|
||||
id: 'splineTypeSelect',
|
||||
value: 'CatmullRomSplineROI',
|
||||
values: [
|
||||
{
|
||||
id: 'CatmullRomSplineROI',
|
||||
value: 'CatmullRomSplineROI',
|
||||
label: 'Catmull Rom Spline',
|
||||
},
|
||||
{ id: 'LinearSplineROI', value: 'LinearSplineROI', label: 'Linear Spline' },
|
||||
{ id: 'BSplineROI', value: 'BSplineROI', label: 'B-Spline' },
|
||||
],
|
||||
commands: {
|
||||
commandName: 'setToolActiveToolbar',
|
||||
commandOptions: {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: 1, // Left Click
|
||||
},
|
||||
{
|
||||
mouseButton: 1, // Left Click+Shift to create a hole
|
||||
modifierKey: 16, // Shift
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Simplified Spline',
|
||||
type: 'switch',
|
||||
id: 'simplifiedSpline',
|
||||
value: true,
|
||||
commands: {
|
||||
commandName: 'setSimplifiedSplineForSplineContourSegmentationTool',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Interpolate Contours',
|
||||
type: 'switch',
|
||||
id: 'splineInterpolateContours',
|
||||
value: false,
|
||||
commands: {
|
||||
commandName: 'setInterpolationToolConfiguration',
|
||||
commandOptions: {
|
||||
toolNames: ['CatmullRomSplineROI', 'LinearSplineROI', 'BSplineROI'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SculptorTool',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'icon-tool-sculptor',
|
||||
label: 'Sculptor Tool',
|
||||
tooltip: 'Sculptor Tool',
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['SculptorTool'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
'setToolActiveToolbar',
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Contour',
|
||||
},
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Dynamic Cursor Size',
|
||||
type: 'switch',
|
||||
id: 'dynamicCursorSize',
|
||||
value: true,
|
||||
commands: {
|
||||
commandName: 'setDynamicCursorSizeForSculptorTool',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Brush',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'icon-tool-brush',
|
||||
label: i18n.t('Buttons:Brush'),
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircularBrush', 'SphereBrush'],
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircularBrush', 'SphereBrush'],
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
commands: {
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
@ -420,10 +671,12 @@ const toolbarButtons: Button[] = [
|
||||
max: 99.5,
|
||||
step: 0.5,
|
||||
value: 25,
|
||||
commands: {
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||
},
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setBrushSize',
|
||||
commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Shape',
|
||||
@ -434,44 +687,72 @@ const toolbarButtons: Button[] = [
|
||||
{ value: 'CircularBrush', label: 'Circle' },
|
||||
{ value: 'SphereBrush', label: 'Sphere' },
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
commands: ['setToolActiveToolbar'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'InterpolateLabelmap',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'icon-tool-interpolation',
|
||||
icon: 'actions-interpolate',
|
||||
label: i18n.t('Buttons:Interpolate Labelmap'),
|
||||
tooltip: i18n.t(
|
||||
'Buttons:Automatically fill in missing slices between drawn segments. Use brush or threshold tools on at least two slices, then click to interpolate across slices. Works in any direction. Volume must be reconstructable.'
|
||||
),
|
||||
evaluate: [
|
||||
'evaluate.cornerstone.segmentation',
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.displaySetIsReconstructable',
|
||||
disabledText: i18n.t('Buttons:The current viewport cannot handle interpolation.'),
|
||||
},
|
||||
],
|
||||
commands: 'interpolateLabelmap',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
'interpolateLabelmap',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SegmentBidirectional',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'icon-tool-bidirectional-segment',
|
||||
icon: 'actions-bidirectional',
|
||||
label: i18n.t('Buttons:Segment Bidirectional'),
|
||||
tooltip: i18n.t(
|
||||
'Buttons:Automatically detects the largest length and width across slices for the selected segment and displays a bidirectional measurement.'
|
||||
),
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
|
||||
},
|
||||
commands: 'runSegmentBidirectional',
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
'runSegmentBidirectional',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -483,12 +764,26 @@ const toolbarButtons: Button[] = [
|
||||
tooltip: i18n.t(
|
||||
'Buttons:Detects segmentable regions with one click. Hover for visual feedback—click when a plus sign appears to auto-segment the lesion.'
|
||||
),
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['RegionSegmentPlus'],
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
|
||||
},
|
||||
commands: 'setToolActiveToolbar',
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['RegionSegmentPlus'],
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
'setToolActiveToolbar',
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -503,7 +798,8 @@ const toolbarButtons: Button[] = [
|
||||
evaluate: [
|
||||
'evaluate.cornerstoneTool.toggle',
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentation',
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
listeners: {
|
||||
@ -512,7 +808,15 @@ const toolbarButtons: Button[] = [
|
||||
),
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: callbacks('LabelmapSlicePropagation'),
|
||||
},
|
||||
commands: 'toggleEnabledDisabledToolbar',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
'toggleEnabledDisabledToolbar',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -529,8 +833,20 @@ const toolbarButtons: Button[] = [
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['MarkerLabelmap', 'MarkerInclude', 'MarkerExclude'],
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
commands: [
|
||||
'setToolActiveToolbar',
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
],
|
||||
commands: 'setToolActiveToolbar',
|
||||
listeners: {
|
||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: callbacks('MarkerLabelmap'),
|
||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: callbacks('MarkerLabelmap'),
|
||||
@ -573,10 +889,16 @@ const toolbarButtons: Button[] = [
|
||||
props: {
|
||||
icon: 'icon-tool-eraser',
|
||||
label: i18n.t('Buttons:Eraser'),
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircularEraser', 'SphereEraser'],
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
@ -603,6 +925,12 @@ const toolbarButtons: Button[] = [
|
||||
commands: 'setToolActiveToolbar',
|
||||
},
|
||||
],
|
||||
commands: {
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -611,15 +939,28 @@ const toolbarButtons: Button[] = [
|
||||
props: {
|
||||
icon: 'icon-tool-threshold',
|
||||
label: 'Threshold Tool',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: [
|
||||
'ThresholdCircularBrush',
|
||||
'ThresholdSphereBrush',
|
||||
'ThresholdCircularBrushDynamic',
|
||||
'ThresholdSphereBrushDynamic',
|
||||
],
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: [
|
||||
'ThresholdCircularBrush',
|
||||
'ThresholdSphereBrush',
|
||||
'ThresholdCircularBrushDynamic',
|
||||
'ThresholdSphereBrushDynamic',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
commands: {
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
@ -728,10 +1069,22 @@ const toolbarButtons: Button[] = [
|
||||
props: {
|
||||
icon: 'icon-tool-shape',
|
||||
label: i18n.t('Buttons:Shapes'),
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable shapes tool.'),
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
disabledText: i18n.t('Buttons:Create new segmentation to enable shapes tool.'),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
commands: {
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: {
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
@ -749,6 +1102,81 @@ const toolbarButtons: Button[] = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SimplifyContours',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'actions-simplify',
|
||||
label: 'Simplify Contours',
|
||||
tooltip: 'Simplify Contours',
|
||||
commands: ['toggleActiveSegmentationUtility'],
|
||||
evaluate: [
|
||||
{
|
||||
name: 'cornerstone.isActiveSegmentationUtility',
|
||||
},
|
||||
],
|
||||
options: 'cornerstone.SimplifyContourOptions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'SmoothContours',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'actions-smooth',
|
||||
label: 'Smooth Contours',
|
||||
tooltip: 'Smooth Contours',
|
||||
commands: ['toggleActiveSegmentationUtility'],
|
||||
evaluate: [
|
||||
{
|
||||
name: 'cornerstone.isActiveSegmentationUtility',
|
||||
},
|
||||
],
|
||||
options: 'cornerstone.SmoothContoursOptions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LogicalContourOperations',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'actions-combine',
|
||||
label: 'Combine Contours',
|
||||
tooltip: 'Combine Contours',
|
||||
commands: ['toggleActiveSegmentationUtility'],
|
||||
evaluate: [
|
||||
{
|
||||
name: 'cornerstone.isActiveSegmentationUtility',
|
||||
},
|
||||
],
|
||||
options: 'cornerstone.LogicalContourOperationsOptions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'LabelMapEditWithContour',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'tool-labelmap-edit-with-contour',
|
||||
label: 'Labelmap Edit with Contour Tool',
|
||||
tooltip: 'Labelmap Edit with Contour Tool',
|
||||
commands: [
|
||||
'setToolActiveToolbar',
|
||||
{
|
||||
commandName: 'activateSelectedSegmentationOfType',
|
||||
commandOptions: { segmentationRepresentationType: 'Labelmap' },
|
||||
},
|
||||
],
|
||||
evaluate: [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['LabelMapEditWithContour'],
|
||||
disabledText: 'Create new segmentation to enable this tool.',
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentationOfType',
|
||||
segmentationRepresentationType: 'Labelmap',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
|
||||
56
modes/segmentation/src/utils/setUpAutoTabSwitchHandler.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Sets up auto tab switching for when the first segmentation is added into the viewer.
|
||||
*/
|
||||
export default function setUpAutoTabSwitchHandler({
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
panelService,
|
||||
}) {
|
||||
const autoTabSwitchEvents = [
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||
];
|
||||
|
||||
// Initially there are no segmentations, so we should switch the tab whenever the first segmentation is added.
|
||||
let shouldSwitchTab = true;
|
||||
|
||||
const unsubscribeAutoTabSwitchEvents = autoTabSwitchEvents
|
||||
.map(eventName =>
|
||||
segmentationService.subscribe(eventName, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
if (!segmentations.length) {
|
||||
// If all the segmentations are removed, then the next time a segmentation is added, we should switch the tab.
|
||||
shouldSwitchTab = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const activeViewportId = viewportGridService.getActiveViewportId();
|
||||
const activeRepresentation = segmentationService
|
||||
.getSegmentationRepresentations(activeViewportId)
|
||||
?.find(representation => representation.active);
|
||||
|
||||
if (activeRepresentation && shouldSwitchTab) {
|
||||
shouldSwitchTab = false;
|
||||
|
||||
switch (activeRepresentation.type) {
|
||||
case 'Labelmap':
|
||||
panelService.activatePanel(
|
||||
'@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap',
|
||||
true
|
||||
);
|
||||
break;
|
||||
case 'Contour':
|
||||
panelService.activatePanel(
|
||||
'@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour',
|
||||
true
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.map(subscription => subscription.unsubscribe);
|
||||
|
||||
return { unsubscribeAutoTabSwitchEvents };
|
||||
}
|
||||
@ -3,3 +3,5 @@ export * from './types';
|
||||
export * from './useViewportRef';
|
||||
export * from './useViewportSize';
|
||||
export * from './useViewportMousePosition';
|
||||
export * from './useActiveToolOptions';
|
||||
export * from './useRunCommand';
|
||||
|
||||
53
platform/core/src/hooks/useActiveToolOptions.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useSystem } from '../contextProviders/SystemProvider';
|
||||
import { ButtonProps } from '../services/ToolBarService/types';
|
||||
import { useToolbar } from './useToolbar';
|
||||
|
||||
type UseActiveToolOptionsProps = {
|
||||
buttonSectionId: string;
|
||||
};
|
||||
|
||||
export function useActiveToolOptions({ buttonSectionId }: UseActiveToolOptionsProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
const { toolbarButtons } = useToolbar({
|
||||
buttonSection: buttonSectionId,
|
||||
});
|
||||
|
||||
// Helper to check a list of buttons for an active tool.
|
||||
const findActiveOptions = (buttons: any[]): unknown => {
|
||||
for (const tool of buttons) {
|
||||
if (tool.componentProps.isActive) {
|
||||
return {
|
||||
activeToolOptions: tool.componentProps.options,
|
||||
activeToolButtonId: tool.componentProps.id,
|
||||
};
|
||||
}
|
||||
if (tool.componentProps.buttonSection) {
|
||||
const nestedButtons = toolbarService.getButtonPropsInButtonSection(
|
||||
tool.componentProps.buttonSection
|
||||
) as ButtonProps[];
|
||||
const activeNested = nestedButtons.find(nested => nested.isActive);
|
||||
if (activeNested) {
|
||||
return {
|
||||
activeToolOptions: activeNested.options,
|
||||
activeToolButtonId: activeNested.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Look for active tool options across all sections.
|
||||
const activeOptions = toolbarButtons.reduce((activeOptions, button) => {
|
||||
if (activeOptions) {
|
||||
return activeOptions;
|
||||
}
|
||||
const sectionId = button.componentProps.buttonSection;
|
||||
const buttons = sectionId ? toolbarService.getButtonSection(sectionId) : [button];
|
||||
return findActiveOptions(buttons);
|
||||
}, null);
|
||||
|
||||
return activeOptions ?? {};
|
||||
}
|
||||
19
platform/core/src/hooks/useRunCommand.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSystem } from '../contextProviders/SystemProvider';
|
||||
|
||||
/**
|
||||
* Hook that provides a runCommand function for executing commands
|
||||
* @returns A memoized runCommand function
|
||||
*/
|
||||
export function useRunCommand() {
|
||||
const { commandsManager } = useSystem();
|
||||
|
||||
const runCommand = useCallback(
|
||||
(commandName: string, commandOptions: Record<string, unknown> = {}) => {
|
||||
return commandsManager.runCommand(commandName, commandOptions);
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
return runCommand;
|
||||
}
|
||||
@ -38,7 +38,10 @@ export const TOOLBAR_SECTIONS = {
|
||||
},
|
||||
|
||||
// mode specific
|
||||
segmentationToolbox: 'segmentationToolbox',
|
||||
labelMapSegmentationToolbox: 'labelMapSegmentationToolbox',
|
||||
contourSegmentationToolbox: 'contourSegmentationToolbox',
|
||||
labelMapSegmentationUtilities: 'labelMapSegmentationUtilities',
|
||||
contourSegmentationUtilities: 'contourSegmentationUtilities',
|
||||
dynamicToolbox: 'dynamic-toolbox',
|
||||
roiThresholdToolbox: 'ROIThresholdToolbox',
|
||||
};
|
||||
@ -314,12 +317,22 @@ export default class ToolbarService extends PubSubService {
|
||||
: undefined;
|
||||
// Check hideWhenDisabled at both evaluateProps level and props level
|
||||
const hideWhenDisabled = evaluateProps?.hideWhenDisabled || props.hideWhenDisabled;
|
||||
|
||||
// Visibility is first determined by the evaluate function. If it is not returned from there,
|
||||
// then we check hideWhenDisabled and disabled to determine visibility.
|
||||
const visible =
|
||||
evaluated?.visible === undefined
|
||||
? hideWhenDisabled && evaluated?.disabled
|
||||
? false
|
||||
: true
|
||||
: evaluated?.visible;
|
||||
|
||||
const updatedProps = {
|
||||
...props,
|
||||
...evaluated,
|
||||
disabled: evaluated?.disabled || false,
|
||||
visible: hideWhenDisabled && evaluated?.disabled ? false : true,
|
||||
className: evaluated?.className || '',
|
||||
visible,
|
||||
className: evaluated?.className || props?.className || '',
|
||||
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
|
||||
};
|
||||
evaluationResults.set(button.id, updatedProps);
|
||||
@ -570,7 +583,8 @@ export default class ToolbarService extends PubSubService {
|
||||
btn.component = buttonType.defaultComponent;
|
||||
}
|
||||
|
||||
if (!buttonType) {
|
||||
if (!buttonType && !btn.component) {
|
||||
console.warn(`Neither button type nor a component found for button: ${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ export type EvaluateObject = {
|
||||
|
||||
export type ButtonOptions = {
|
||||
id: string;
|
||||
type: 'range' | 'radio' | 'double-range' | 'custom';
|
||||
type: 'range' | 'radio' | 'double-range' | 'custom' | 'checkbox' | 'select' | 'button';
|
||||
name?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
@ -59,6 +59,7 @@ export type ButtonProps = {
|
||||
listeners?: Record<string, RunCommand>;
|
||||
options?: ButtonOptions[];
|
||||
buttonSection?: string | boolean;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export type Button = {
|
||||
|
||||
@ -117,6 +117,13 @@ Let's look at one of the evaluators (for `evaluate.cornerstoneTool`)
|
||||
|
||||
as you can see the job of this evaluator is to determine if the button should be disabled or not. It does so by checking the `toolGroup` and the `toolName` and then returns an object with `disabled` and `className` properties.
|
||||
|
||||
Various information can be returned by an evaluator. In particular...
|
||||
- `disabled`: flag indicating if the tool should be disabled or not
|
||||
- `disabledText`: the text or tooltip to show if the `disabled` flag above is set to `true`
|
||||
- `visible`: flag indicating if the tool show be visible or not; this is a convenient way to force a tool to hide based on some custom (evaluator) logic
|
||||
- `isActive`: flag to indicating where the tool is currently active or not
|
||||
- `className`: custom CSS class names to add to the tool's component
|
||||
|
||||
The following evaluators are provided by us:
|
||||
|
||||
- `evaluate.cornerstoneTool`: If assigned to a button (see next), it will make the button react to the active viewport state based on its toolGroup.
|
||||
|
||||
@ -34,6 +34,17 @@ createLabelmapForDisplaySet(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
createContourForDisplaySet(
|
||||
displaySet,
|
||||
{
|
||||
segmentationId?: string,
|
||||
label: string,
|
||||
segments?: {
|
||||
[segmentIndex: number]: Partial<Segment>
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Segmentation Management
|
||||
@ -85,17 +96,33 @@ interface Segmentation {
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Creating a Segmentation
|
||||
### Creating a Label Map Segmentation
|
||||
|
||||
```typescript
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
|
||||
const segmentationId = await segmentationService.createLabelmapForDisplaySet(
|
||||
displaySet,
|
||||
{
|
||||
label: 'New Segmentation',
|
||||
label: 'New Label Map Segmentation',
|
||||
segments: {
|
||||
1: {
|
||||
label: 'First Segment',
|
||||
label: 'First Label Map Segment',
|
||||
active: true
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
### Creating a Label Map Segmentation
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
|
||||
const segmentationId = await segmentationService.createContourForDisplaySet(
|
||||
displaySet,
|
||||
{
|
||||
label: 'New Contour Segmentation',
|
||||
segments: {
|
||||
1: {
|
||||
label: 'First Contour Segment',
|
||||
active: true
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,16 +3,25 @@
|
||||
"Add new segmentation": "Add new segmentation",
|
||||
"Add segment": "Add segment",
|
||||
"Add segmentation": "Add segmentation",
|
||||
"Contour": "Contour",
|
||||
"Contour tools": "Contour tools",
|
||||
"Contour Segmentations": "Contour segmentations",
|
||||
"Delete": "Delete",
|
||||
"Display inactive segmentations": "Display inactive segmentations",
|
||||
"Download DICOM RTSS": "Download DICOM RTSS",
|
||||
"Export DICOM SEG": "Export DICOM SEG",
|
||||
"Download DICOM SEG": "Download DICOM SEG",
|
||||
"Download DICOM RTSTRUCT": "Download DICOM RTSTRUCT",
|
||||
"Fill": "Fill",
|
||||
"Inactive segmentations": "Inactive segmentations",
|
||||
"Labelmap": "Label map",
|
||||
"Labelmap tools": "Label map tools",
|
||||
"Labelmap Segmentations": "Label map segmentations",
|
||||
"Opacity": "Opacity",
|
||||
"Outline": "Outline",
|
||||
"Rename": "Rename",
|
||||
"Segmentation": "Segmentation",
|
||||
"Segmentations": "Segmentations",
|
||||
"Segmentation not supported": "Segmentation not supported",
|
||||
"Size": "Size"
|
||||
}
|
||||
|
||||
@ -3,16 +3,24 @@
|
||||
"Add new segmentation": "Test Add new segmentation",
|
||||
"Add segment": "Test Add segment",
|
||||
"Add segmentation": "Test Add segmentation",
|
||||
"Contour": "Test Contour",
|
||||
"Contour tools": "Test Contour tools",
|
||||
"Contour Segmentations": "Contour segmentations",
|
||||
"Delete": "Test Delete",
|
||||
"Display inactive segmentations": "Test Display inactive segmentations",
|
||||
"Download DICOM RTSS": "Test Download DICOM RTSS",
|
||||
"Export DICOM SEG": "Test Export DICOM SEG",
|
||||
"Download DICOM SEG": "Test Download DICOM SEG",
|
||||
"Download DICOM RTSTRUCT": "Test Download DICOM RTSTRUCT",
|
||||
"Fill": "Test Fill",
|
||||
"Inactive segmentations": "Test Inactive segmentations",
|
||||
"Labelmap": "Test Label map",
|
||||
"Labelmap tools": "Test Label map tools",
|
||||
"Labelmap Segmentations": "Test Label map segmentations",
|
||||
"Opacity": "Test Opacity",
|
||||
"Outline": "Test Outline",
|
||||
"Rename": "Test Rename",
|
||||
"Segmentation": "Test Segmentation",
|
||||
"Segmentation not supported": "Test Segmentation not supported",
|
||||
"Size": "Test Size"
|
||||
}
|
||||
|
||||
@ -87,7 +87,10 @@ interface DataRowProps {
|
||||
description: string;
|
||||
details?: { primary: string[]; secondary: string[] };
|
||||
//
|
||||
/** Primary selection: selected and in the active segmentation */
|
||||
isSelected?: boolean;
|
||||
/** Secondary selection: selected but in an inactive segmentation */
|
||||
isSecondarySelected?: boolean;
|
||||
onSelect?: (e) => void;
|
||||
//
|
||||
isVisible: boolean;
|
||||
@ -103,6 +106,7 @@ interface DataRowProps {
|
||||
//
|
||||
colorHex?: string;
|
||||
onColor: (e) => void;
|
||||
onCopy?: (e) => void;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
@ -121,7 +125,9 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
|
||||
onRename,
|
||||
onDelete,
|
||||
onColor,
|
||||
onCopy,
|
||||
isSelected = false,
|
||||
isSecondarySelected = false,
|
||||
isVisible = true,
|
||||
disableEditing = false,
|
||||
className,
|
||||
@ -146,6 +152,9 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
|
||||
case 'Rename':
|
||||
onRename(e);
|
||||
break;
|
||||
case 'Copy':
|
||||
onCopy?.(e);
|
||||
break;
|
||||
case 'Lock':
|
||||
onToggleLocked(e);
|
||||
break;
|
||||
@ -234,7 +243,11 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
|
||||
onClick={onSelect}
|
||||
data-cy="data-row"
|
||||
>
|
||||
{/* Hover Overlay */}
|
||||
{/* Secondary Selection Tint (below hover, always visible when secondary-selected) */}
|
||||
{isSecondarySelected && (
|
||||
<div className="bg-primary/20 pointer-events-none absolute inset-0"></div>
|
||||
)}
|
||||
|
||||
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
|
||||
|
||||
{/* Number Box */}
|
||||
@ -351,6 +364,17 @@ const DataRowComponent = React.forwardRef<HTMLDivElement, DataRowProps>(
|
||||
Rename
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{onCopy && (
|
||||
<DropdownMenuItem onClick={e => handleAction('Copy', e)}>
|
||||
<Icons.Copy className="text-foreground" />
|
||||
<span
|
||||
className="pl-2"
|
||||
data-cy="Duplicate"
|
||||
>
|
||||
Duplicate
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={e => handleAction('Delete', e)}>
|
||||
<Icons.Delete className="text-foreground" />
|
||||
<span
|
||||
|
||||
@ -146,6 +146,8 @@ import {
|
||||
ToolExpand,
|
||||
ToolClickSegment,
|
||||
ToolSegmentLabel,
|
||||
ToolSculptor,
|
||||
ToolLabelmapEditWithContour,
|
||||
} from './Sources/Tools';
|
||||
import ActionNewDialog from './Sources/ActionNewDialog';
|
||||
import NotificationInfo from './Sources/NotificationInfo';
|
||||
@ -228,6 +230,7 @@ import ArrowRight from './Sources/ArrowRight';
|
||||
import ChevronLeft from './Sources/ChevronLeft';
|
||||
import StatusAlert from './Sources/StatusAlert';
|
||||
import Undo from './Sources/Undo';
|
||||
import TabContours from './Sources/TabContours';
|
||||
//
|
||||
//
|
||||
type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
@ -674,6 +677,8 @@ export const Icons = {
|
||||
'status-tracked': (props: IconProps) => StatusTracking(props),
|
||||
'status-untracked': (props: IconProps) => StatusUntracked(props),
|
||||
'status-locked': (props: IconProps) => StatusLocked(props),
|
||||
'tab-contours': (props: IconProps) => TabContours(props),
|
||||
TabContours: (props: IconProps) => TabContours(props),
|
||||
'tab-segmentation': (props: IconProps) => TabSegmentation(props),
|
||||
'tab-studies': (props: IconProps) => TabStudies(props),
|
||||
'tab-linear': (props: IconProps) => TabLinear(props),
|
||||
@ -720,6 +725,7 @@ export const Icons = {
|
||||
'tool-referenceLines': (props: IconProps) => ToolReferenceLines(props),
|
||||
'tool-reset': (props: IconProps) => ToolReset(props),
|
||||
'tool-rotate-right': (props: IconProps) => ToolRotateRight(props),
|
||||
'icon-tool-sculptor': (props: IconProps) => ToolSculptor(props),
|
||||
'tool-seg-brush': (props: IconProps) => ToolSegBrush(props),
|
||||
'tool-seg-eraser': (props: IconProps) => ToolSegEraser(props),
|
||||
'tool-seg-shape': (props: IconProps) => ToolSegShape(props),
|
||||
@ -775,6 +781,7 @@ export const Icons = {
|
||||
'old-trash': (props: IconProps) => Trash(props),
|
||||
'tool-point': (props: IconProps) => ToolCircle(props),
|
||||
'tool-freehand-line': (props: IconProps) => ToolFreehand(props),
|
||||
'tool-labelmap-edit-with-contour': (props: IconProps) => ToolLabelmapEditWithContour(props),
|
||||
'actions-smooth': (props: IconProps) => ActionsSmooth(props),
|
||||
'actions-simplify': (props: IconProps) => ActionsSimplify(props),
|
||||
'actions-combine': (props: IconProps) => ActionsCombine(props),
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '../types';
|
||||
|
||||
export const TabContours = (props: IconProps) => (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.9168 1.5H10.2502C10.02 1.5 9.8335 1.68655 9.8335 1.91667V3.58333C9.8335 3.81345 10.02 4 10.2502 4H11.9168C12.1469 4 12.3335 3.81345 12.3335 3.58333V1.91667C12.3335 1.68655 12.1469 1.5 11.9168 1.5Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.9168 18.1667H10.2502C10.02 18.1667 9.8335 18.3532 9.8335 18.5833V20.25C9.8335 20.4801 10.02 20.6667 10.2502 20.6667H11.9168C12.1469 20.6667 12.3335 20.4801 12.3335 20.25V18.5833C12.3335 18.3532 12.1469 18.1667 11.9168 18.1667Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.2503 9.83333H18.5837C18.3535 9.83333 18.167 10.0199 18.167 10.25V11.9167C18.167 12.1468 18.3535 12.3333 18.5837 12.3333H20.2503C20.4804 12.3333 20.667 12.1468 20.667 11.9167V10.25C20.667 10.0199 20.4804 9.83333 20.2503 9.83333Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3.58333 9.83333H1.91667C1.68655 9.83333 1.5 10.0199 1.5 10.25V11.9167C1.5 12.1468 1.68655 12.3333 1.91667 12.3333H3.58333C3.81345 12.3333 4 12.1468 4 11.9167V10.25C4 10.0199 3.81345 9.83333 3.58333 9.83333Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.84277 9.83332C3.11056 8.0751 3.93334 6.44851 5.19101 5.191C6.44867 3.93349 8.07536 3.1109 9.83361 2.84332"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.83361 19.3242C8.07524 19.0565 6.44846 18.2338 5.19078 16.9762C3.93311 15.7185 3.1104 14.0917 2.84277 12.3333"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19.3243 12.3333C19.0568 14.0916 18.2341 15.7184 16.9764 16.9759C15.7187 18.2335 14.0918 19.056 12.3335 19.3233"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.3335 2.84332C14.0917 3.1109 15.7184 3.93349 16.9761 5.191C18.2338 6.44851 19.0565 8.0751 19.3243 9.83332"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default TabContours;
|
||||
@ -2995,16 +2995,16 @@ export const ToolSegmentLabel = (props: IconProps) => (
|
||||
<path
|
||||
d="M2.6636 14.3578C2.58841 14.4105 2.53094 14.4847 2.49878 14.5707C2.46661 14.6567 2.46127 14.7504 2.48345 14.8395C2.50564 14.9286 2.5543 15.0088 2.62302 15.0697C2.69175 15.1305 2.77731 15.1691 2.86842 15.1804L7.42867 16.5722L8.82125 21.1316C8.83209 21.2229 8.87047 21.3088 8.93131 21.3779C8.99214 21.4469 9.07254 21.4958 9.16181 21.518C9.25109 21.5402 9.34502 21.5348 9.43111 21.5023C9.5172 21.4699 9.59139 21.412 9.6438 21.3364L20.4127 10.5683C20.7607 10.1801 20.9661 9.68484 20.9951 9.16423L21 3.82256C21 3.6044 20.9133 3.39518 20.7591 3.24092C20.6048 3.08666 20.3956 3 20.1774 3H14.8382C14.3176 3.02823 13.8223 3.2334 13.4341 3.58155L2.6636 14.3578Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M16.8882 8.75787C15.9796 8.75787 15.2431 8.02133 15.2431 7.11276C15.2431 6.20419 15.9796 5.46765 16.8882 5.46765C17.7968 5.46765 18.5333 6.20419 18.5333 7.11276C18.5333 8.02133 17.7968 8.75787 16.8882 8.75787Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -3544,3 +3544,57 @@ export const ToolContract = (props: IconProps) => (
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export const ToolSculptor = (props: IconProps) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.5 1.5C19.2614 1.5 21.5 3.73858 21.5 6.5C21.5 8.59844 20.2065 10.3931 18.374 11.1348C18.4555 11.5776 18.5 12.0336 18.5 12.5C18.5 16.6421 15.1421 20 11 20C10.6311 20 10.4428 19.5684 10.6035 19.2363C10.8575 18.7113 11 18.1224 11 17.5C11 15.2909 9.20914 13.5 7 13.5C6.07751 13.5 5.22759 13.8124 4.55078 14.3369C4.24235 14.5757 3.75506 14.4797 3.67188 14.0986C3.55945 13.5836 3.5 13.0488 3.5 12.5C3.5 8.35786 6.85786 5 11 5C11.2421 5 11.4815 5.01272 11.7178 5.03516C12.3439 2.98844 14.2482 1.5 16.5 1.5ZM16.4707 3.53125C16.1133 3.53125 15.8906 3.7832 15.8906 4.14648V6.07422H14.0684C13.7109 6.07422 13.4648 6.29688 13.4648 6.64844C13.4648 7 13.7109 7.2168 14.0684 7.2168H15.8906V9.13867C15.8906 9.50195 16.1133 9.75391 16.4707 9.75391C16.8223 9.75391 17.0449 9.50195 17.0449 9.13867V7.2168H18.8613C19.2188 7.2168 19.4707 7 19.4707 6.64844C19.4707 6.29688 19.2188 6.07422 18.8613 6.07422H17.0449V4.14648C17.0449 3.7832 16.8223 3.53125 16.4707 3.53125Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M5.69531 18.1406C5.34375 18.1406 5.08594 17.9238 5.08594 17.5664C5.08594 17.2148 5.34375 16.998 5.69531 16.998H8.50195C8.85352 16.998 9.11719 17.2148 9.11719 17.5664C9.11719 17.9238 8.85352 18.1406 8.50195 18.1406H5.69531Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle
|
||||
cx="7"
|
||||
cy="17.5"
|
||||
r="4.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export const ToolLabelmapEditWithContour = (props: IconProps) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10.3945 12.6113C10.043 12.6113 9.78516 12.3945 9.78516 12.0371C9.78516 11.6855 10.043 11.4688 10.3945 11.4688H13.2012C13.5527 11.4688 13.8164 11.6855 13.8164 12.0371C13.8164 12.3945 13.5527 12.6113 13.2012 12.6113H10.3945Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
opacity="0.5"
|
||||
d="M18.312 6.84473C21.8473 8.88583 22.9225 13.6424 20.7134 17.4687C18.5042 21.2951 13.8473 22.7422 10.312 20.7011C9.51661 20.2419 8.84737 19.6439 8.31042 18.9512C11.3012 19.3598 14.4827 17.8251 16.1946 14.8598C17.9067 11.8945 17.6457 8.3712 15.7963 5.98538C16.6645 6.10409 17.5167 6.38556 18.312 6.84473Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle
|
||||
cx="13.7852"
|
||||
cy="13.4688"
|
||||
r="8"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.50586 8.22266C4.14844 8.22266 3.92578 7.9707 3.92578 7.60742V5.68555H2.10352C1.74609 5.68555 1.5 5.46875 1.5 5.11719C1.5 4.76562 1.74609 4.54297 2.10352 4.54297H3.92578V2.61523C3.92578 2.25195 4.14844 2 4.50586 2C4.85742 2 5.08008 2.25195 5.08008 2.61523V4.54297H6.89648C7.25391 4.54297 7.50586 4.76562 7.50586 5.11719C7.50586 5.46875 7.25391 5.68555 6.89648 5.68555H5.08008V7.60742C5.08008 7.9707 4.85742 8.22266 4.50586 8.22266Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import RowInputRange from './RowInputRange';
|
||||
import RowSegmentedControl from './RowSegmentedControl';
|
||||
import RowDoubleRange from './RowDoubleRange';
|
||||
import { Button } from '../Button';
|
||||
import { Checkbox } from '../Checkbox/Checkbox';
|
||||
import { Label } from '../Label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../Select';
|
||||
import { Switch } from '../Switch';
|
||||
|
||||
const SETTING_TYPES = {
|
||||
RANGE: 'range',
|
||||
@ -10,6 +14,9 @@ const SETTING_TYPES = {
|
||||
CUSTOM: 'custom',
|
||||
DOUBLE_RANGE: 'double-range',
|
||||
BUTTON: 'button',
|
||||
CHECKBOX: 'checkbox',
|
||||
SWITCH: 'switch',
|
||||
SELECT: 'select',
|
||||
};
|
||||
|
||||
function ToolSettings({ options }) {
|
||||
@ -18,7 +25,8 @@ function ToolSettings({ options }) {
|
||||
}
|
||||
|
||||
if (typeof options === 'function') {
|
||||
return options();
|
||||
const OptionsComponent = options;
|
||||
return <OptionsComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -39,6 +47,12 @@ function ToolSettings({ options }) {
|
||||
return renderCustomSetting(option);
|
||||
case SETTING_TYPES.BUTTON:
|
||||
return renderButtonSetting(option);
|
||||
case SETTING_TYPES.CHECKBOX:
|
||||
return renderCheckboxSetting(option);
|
||||
case SETTING_TYPES.SWITCH:
|
||||
return renderSwitchSetting(option);
|
||||
case SETTING_TYPES.SELECT:
|
||||
return renderSelectSetting(option);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -116,4 +130,74 @@ const renderButtonSetting = option => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderCheckboxSetting = option => {
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={option.value}
|
||||
onCheckedChange={checked => {
|
||||
option.onChange?.(checked);
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={option.id}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{option.name}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSwitchSetting = option => {
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Switch
|
||||
id={option.id}
|
||||
checked={option.value}
|
||||
onCheckedChange={checked => {
|
||||
option.onChange?.(checked);
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={option.id}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{option.name}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSelectSetting = option => {
|
||||
return (
|
||||
<Select
|
||||
key={option.id}
|
||||
onValueChange={value => option.onChange?.(value)}
|
||||
value={option.value}
|
||||
>
|
||||
<SelectTrigger className="w-full overflow-hidden">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{option.values.map(value => (
|
||||
<SelectItem
|
||||
key={value.id}
|
||||
value={value.id}
|
||||
>
|
||||
{value.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSettings;
|
||||
|
||||
@ -8,12 +8,21 @@ export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
|
||||
const { onSegmentationAdd, data, disableEditing, mode, disabled } =
|
||||
useSegmentationTableContext('AddSegmentationRow');
|
||||
const {
|
||||
onSegmentationAdd,
|
||||
data,
|
||||
disableEditing,
|
||||
mode,
|
||||
disabled,
|
||||
segmentationRepresentationType,
|
||||
} = useSegmentationTableContext('AddSegmentationRow');
|
||||
|
||||
const isEmpty = data.length === 0;
|
||||
// Check if we have at least one segmentation of the representation type for the panel this component is contained in.
|
||||
const hasRepresentationType =
|
||||
(!segmentationRepresentationType && data.length > 0) ||
|
||||
data.some(info => segmentationRepresentationType === info.representation?.type);
|
||||
|
||||
if (!isEmpty && mode === 'collapsed') {
|
||||
if (hasRepresentationType && mode === 'collapsed') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -25,7 +34,9 @@ export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
|
||||
<div
|
||||
data-cy="addSegmentation"
|
||||
className={`group ${disabled ? 'pointer-events-none cursor-not-allowed opacity-70' : ''}`}
|
||||
onClick={() => !disabled && onSegmentationAdd('')}
|
||||
onClick={() =>
|
||||
!disabled && onSegmentationAdd({ segmentationId: '', segmentationRepresentationType })
|
||||
}
|
||||
>
|
||||
{children}
|
||||
<div className="text-primary group-hover:bg-secondary-dark flex items-center rounded-[4px] pl-1 group-hover:cursor-pointer">
|
||||
@ -33,7 +44,7 @@ export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
|
||||
{disabled ? <Icons.Info /> : <Icons.Add />}
|
||||
</div>
|
||||
<span className="text-[13px]">
|
||||
{t(`${disabled ? 'Segmentation Not Supported' : 'Add Segmentation'}`)}
|
||||
{t(disabled ? 'Segmentation not supported' : 'Add segmentation')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import React from 'react';
|
||||
import { PanelSection } from '../PanelSection';
|
||||
import { useSegmentationTableContext, SegmentationExpandedProvider } from './contexts';
|
||||
import {
|
||||
useSegmentationTableContext,
|
||||
SegmentationExpandedProvider,
|
||||
useSegmentationExpanded,
|
||||
} from './contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
@ -46,23 +50,31 @@ const SegmentationCollapsedDropdownMenu = ({ children }: { children: React.React
|
||||
// Selector component - for the segmentation selection dropdown
|
||||
const SegmentationCollapsedSelector = () => {
|
||||
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
|
||||
const { data, activeSegmentationId, onSegmentationClick } = useSegmentationTableContext(
|
||||
const { data, onSegmentationClick, segmentationRepresentationType } = useSegmentationTableContext(
|
||||
'SegmentationCollapsedSelector'
|
||||
);
|
||||
const { segmentation } = useSegmentationExpanded('SegmentationCollapsedSelector');
|
||||
|
||||
if (!data?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segmentations = data.map(seg => ({
|
||||
id: seg.segmentation.segmentationId,
|
||||
label: seg.segmentation.label,
|
||||
}));
|
||||
const segmentations = data
|
||||
// Only show segmentations of the representation type for this panel. Show all segmentations if no type is specified.
|
||||
.filter(
|
||||
seg =>
|
||||
!segmentationRepresentationType ||
|
||||
segmentationRepresentationType === seg.representation.type
|
||||
)
|
||||
.map(seg => ({
|
||||
id: seg.segmentation.segmentationId,
|
||||
label: seg.segmentation.label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
onValueChange={value => onSegmentationClick(value)}
|
||||
value={activeSegmentationId}
|
||||
value={segmentation?.segmentationId}
|
||||
>
|
||||
<SelectTrigger className="w-full overflow-hidden">
|
||||
<SelectValue placeholder={t('Select a segmentation')} />
|
||||
@ -120,28 +132,33 @@ const SegmentationCollapsedContent = ({ children }: { children: React.ReactNode
|
||||
const SegmentationCollapsedRoot: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children = null,
|
||||
}) => {
|
||||
const { mode, data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsed');
|
||||
const { mode, data, segmentationRepresentationType, selectedSegmentationIdForType } =
|
||||
useSegmentationTableContext('SegmentationCollapsed');
|
||||
|
||||
// Check if we should render based on mode
|
||||
if (mode !== 'collapsed' || !data || data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find active segmentation
|
||||
const activeSegmentationInfo = data.find(
|
||||
info => info.segmentation.segmentationId === activeSegmentationId
|
||||
// Find the segmentations for the representation type for this collapsed view.
|
||||
const segmentations = data.filter(
|
||||
segmentation =>
|
||||
!segmentationRepresentationType ||
|
||||
segmentationRepresentationType === segmentation.representation?.type
|
||||
);
|
||||
|
||||
if (!activeSegmentationInfo) {
|
||||
// Check if we should render.
|
||||
if (mode !== 'collapsed' || !data || data.length === 0 || segmentations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the selected segmentation info for the representation type, or default to the first one.
|
||||
const selectedSegmentationInfo =
|
||||
segmentations.find(
|
||||
segmentation => segmentation.segmentation.segmentationId === selectedSegmentationIdForType
|
||||
) ?? segmentations[0];
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
<PanelSection className="mb-0">
|
||||
<SegmentationExpandedProvider
|
||||
segmentation={activeSegmentationInfo.segmentation}
|
||||
representation={activeSegmentationInfo.representation}
|
||||
segmentation={selectedSegmentationInfo.segmentation}
|
||||
representation={selectedSegmentationInfo.representation}
|
||||
isActive={true}
|
||||
onSegmentationClick={() => {}} // No-op since it's already the active one
|
||||
>
|
||||
|
||||
@ -93,7 +93,7 @@ const SegmentationExpandedContent = ({ children }: { children: React.ReactNode }
|
||||
|
||||
// Main compound component
|
||||
const SegmentationExpandedRoot = ({ children }) => {
|
||||
const { data, activeSegmentationId, onSegmentationClick, mode } =
|
||||
const { data, activeSegmentationId, onSegmentationClick, mode, segmentationRepresentationType } =
|
||||
useSegmentationTableContext('SegmentationExpanded');
|
||||
|
||||
const { ref: scrollableContainerRef, maxHeight } = useDynamicMaxHeight(data);
|
||||
@ -113,25 +113,31 @@ const SegmentationExpandedRoot = ({ children }) => {
|
||||
style={{ maxHeight: maxHeight }}
|
||||
className={`space-y-0 pl-0.5`}
|
||||
>
|
||||
{data.map(segmentationInfo => {
|
||||
const isActive = segmentationInfo.segmentation.segmentationId === activeSegmentationId;
|
||||
{data
|
||||
.filter(
|
||||
segmentationInfo =>
|
||||
!segmentationRepresentationType ||
|
||||
segmentationInfo.representation.type === segmentationRepresentationType
|
||||
)
|
||||
.map(segmentationInfo => {
|
||||
const isActive = segmentationInfo.segmentation.segmentationId === activeSegmentationId;
|
||||
|
||||
return (
|
||||
<PanelSection
|
||||
key={segmentationInfo.segmentation.segmentationId}
|
||||
className=""
|
||||
>
|
||||
<SegmentationExpandedProvider
|
||||
segmentation={segmentationInfo.segmentation}
|
||||
representation={segmentationInfo.representation}
|
||||
isActive={isActive}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
return (
|
||||
<PanelSection
|
||||
key={segmentationInfo.segmentation.segmentationId}
|
||||
className=""
|
||||
>
|
||||
{children}
|
||||
</SegmentationExpandedProvider>
|
||||
</PanelSection>
|
||||
);
|
||||
})}
|
||||
<SegmentationExpandedProvider
|
||||
segmentation={segmentationInfo.segmentation}
|
||||
representation={segmentationInfo.representation}
|
||||
isActive={isActive}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
>
|
||||
{children}
|
||||
</SegmentationExpandedProvider>
|
||||
</PanelSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
@ -15,6 +15,7 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
onSegmentClick,
|
||||
onSegmentEdit,
|
||||
onSegmentDelete,
|
||||
onSegmentCopy,
|
||||
data,
|
||||
showSegmentIndex = true,
|
||||
} = useSegmentationTableContext('SegmentationSegments');
|
||||
@ -116,6 +117,9 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
const { locked, active, label, displayText } = segmentFromSegmentation;
|
||||
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
|
||||
|
||||
// Secondary selection: segment is active, but its parent segmentation is inactive
|
||||
const isSecondarySelected = active && !isActiveSegmentation;
|
||||
|
||||
const hasStats = segmentFromSegmentation.cachedStats?.namedStats;
|
||||
|
||||
const segmentRowRef = (element: HTMLElement) => {
|
||||
@ -139,7 +143,10 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
// details={displayText}
|
||||
description={displayText}
|
||||
colorHex={cssColor}
|
||||
isSelected={active}
|
||||
// Primary selection only when part of the active segmentation
|
||||
isSelected={active && isActiveSegmentation}
|
||||
// Secondary selection tint when selected in an inactive segmentation
|
||||
isSecondarySelected={isSecondarySelected}
|
||||
isVisible={visible}
|
||||
isLocked={locked}
|
||||
disableEditing={disableEditing}
|
||||
@ -158,6 +165,11 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
|
||||
onSelect={() => onSegmentClick(segmentation.segmentationId, segmentIndex)}
|
||||
onRename={() => onSegmentEdit(segmentation.segmentationId, segmentIndex)}
|
||||
onDelete={() => onSegmentDelete(segmentation.segmentationId, segmentIndex)}
|
||||
onCopy={
|
||||
onSegmentCopy
|
||||
? () => onSegmentCopy(segmentation.segmentationId, segmentIndex)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -65,6 +65,13 @@ export interface SegmentationTableContextType {
|
||||
showSegmentIndex?: boolean;
|
||||
renderInactiveSegmentations?: boolean;
|
||||
|
||||
// The type segmentations displayed/filtered in this table. If undefined, show all types.
|
||||
segmentationRepresentationType?: string;
|
||||
|
||||
// The (last) selected segmentation ID for the representation type above.
|
||||
// If the type above is undefined, then it will store the last active segmentation ID.
|
||||
selectedSegmentationIdForType?: string;
|
||||
|
||||
// Function handlers
|
||||
setShowConfig?: (show: boolean) => void;
|
||||
setRenderFill?: ({ type }: { type: string }, value: boolean) => void;
|
||||
@ -73,12 +80,19 @@ export interface SegmentationTableContextType {
|
||||
setFillAlpha?: ({ type }: { type: string }, value: number) => void;
|
||||
setFillAlphaInactive?: ({ type }: { type: string }, value: number) => void;
|
||||
toggleRenderInactiveSegmentations?: () => void;
|
||||
onSegmentationAdd?: (segmentationId: string) => void;
|
||||
onSegmentationAdd?: ({
|
||||
segmentationId,
|
||||
segmentationRepresentationType,
|
||||
}: {
|
||||
segmentationId: string;
|
||||
segmentationRepresentationType: string;
|
||||
}) => void;
|
||||
onSegmentationClick?: (segmentationId: string) => void;
|
||||
onSegmentationDelete?: (segmentationId: string) => void;
|
||||
onSegmentAdd?: (segmentationId: string) => void;
|
||||
onSegmentClick?: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentEdit?: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentCopy?: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentationEdit?: (segmentationId: string) => void;
|
||||
onSegmentColorClick?: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentDelete?: (segmentationId: string, segmentIndex: number) => void;
|
||||
|
||||
@ -22,6 +22,8 @@ test('should prevent editing of label map segmentations when panelSegmentation.d
|
||||
}
|
||||
);
|
||||
});
|
||||
await page.getByTestId('panelSegmentationWithToolsLabelMap-btn').click();
|
||||
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
@ -74,6 +76,9 @@ test('should allow editing of label map segmentations when panelSegmentation.dis
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await page.getByTestId('panelSegmentationWithToolsLabelMap-btn').click();
|
||||
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
@ -13,11 +13,13 @@ test('should properly display MPR for MR', async ({ page }) => {
|
||||
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
await checkForScreenshot(page, page, screenShotPaths.segHydrationThenMPR.segPostHydration);
|
||||
|
||||
await page.getByTestId('Layout').click();
|
||||
await page.getByTestId('Axial Primary').click();
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
await checkForScreenshot(
|
||||
page,
|
||||
page,
|
||||
|
||||
@ -10,9 +10,12 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test('checks basic add, rename, delete segments from panel', async ({ page }) => {
|
||||
// Segmentation Panel should already be open
|
||||
const segmentationPanel = page.getByTestId('panelSegmentationWithTools-btn');
|
||||
const segmentationPanel = page.getByTestId('panelSegmentationWithToolsLabelMap-btn');
|
||||
await expect(segmentationPanel).toBeVisible();
|
||||
|
||||
// Switch to labelmap tab.
|
||||
segmentationPanel.click();
|
||||
|
||||
// Add segmentation
|
||||
const addSegmentationBtn = page.getByTestId('addSegmentation');
|
||||
await addSegmentationBtn.click();
|
||||
@ -72,7 +75,7 @@ test('checks saved segmentations loads and jumps to slices', async ({ page }) =>
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
|
||||
// Segmentation Panel should already be open
|
||||
const segmentationPanel = page.getByTestId('panelSegmentationWithTools-btn');
|
||||
const segmentationPanel = page.getByTestId('panelSegmentationWithToolsLabelMap-btn');
|
||||
await expect(segmentationPanel).toBeVisible();
|
||||
|
||||
// Confirm spleen jumps to slice 17
|
||||
|
||||
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 420 KiB |