* Update playwright screenshots and fix broken tests. --------- Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
247 lines
7.4 KiB
TypeScript
247 lines
7.4 KiB
TypeScript
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 { useTranslation } from 'react-i18next';
|
|
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;
|
|
}) {
|
|
const { t } = useTranslation('SegmentationPanel');
|
|
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={t(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 { t } = useTranslation('SegmentationPanel');
|
|
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>{t(operation.label)}</div>
|
|
</div>
|
|
<div className="bg-muted 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();
|
|
}}
|
|
>
|
|
{t(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">
|
|
{t('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={t('New segment name')}
|
|
value={newSegmentName}
|
|
onChange={e => setNewSegmentName(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default LogicalContourOperationOptions;
|