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>
This commit is contained in:
73 files changed
+2770
-435
No files matched your search
@@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
+15
-1
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user