feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865)

This commit is contained in:
Alireza authored and GitHub committed 2025-03-17 22:34:34 -04:00
1 parent 694f3e9611
commit ec79b5523e
73 files changed
+3603 -1563

No files matched your search

@@ -1,26 +1,8 @@
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
/**
* Props interface for the AddSegmentRow component
*/
interface AddSegmentRowProps {
/** Optional child elements to render within the row */
children?: React.ReactNode;
/** Optional segmentation object to override the active segmentation */
segmentation?: unknown;
}
/**
* A component that renders a row with controls for adding segments and toggling visibility
* in the segmentation table.
*
* @param props - Component properties
* @param props.children - Optional child elements to render within the row
* @param props.segmentation - Optional segmentation object to override the active segmentation
*/
export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, segmentation }) => {
export const AddSegmentRow: React.FC<{ children?: React.ReactNode }> = ({ children = null }) => {
const {
activeRepresentation,
disableEditing,
@@ -29,18 +11,32 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
onToggleSegmentationRepresentationVisibility,
data,
showAddSegment,
} = useSegmentationTableContext('SegmentationTable');
} = useSegmentationTableContext('AddSegmentRow');
const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every(
segment => segment?.visible !== false
);
// Try to get from expanded context first, then fall back to active segmentation
let segmentationId = activeSegmentationId;
let representation = activeRepresentation;
const segmentationIdToUse = segmentation ? segmentation.segmentationId : activeSegmentationId;
try {
const expandedContext = useSegmentationExpanded('AddSegmentRow');
if (expandedContext.isActive) {
segmentationId = expandedContext.segmentation.segmentationId;
representation = expandedContext.representation;
}
} catch (e) {
// Use the default values from table context
}
// If no segmentations, don't render
if (!data?.length) {
return null;
}
// Check if all segments are visible
const allSegmentsVisible = Object.values(representation?.segments || {}).every(
segment => segment?.visible !== false
);
const Icon = allSegmentsVisible ? (
<Icons.Hide className="h-6 w-6" />
) : (
@@ -50,14 +46,14 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
const allowAddSegment = showAddSegment && !disableEditing;
return (
<div className="bg-primary-dark my-px flex h-7 w-full items-center justify-between rounded pl-0.5 pr-7">
<div className="my-px flex h-7 w-full items-center justify-between rounded pl-0.5 pr-7">
<div className="flex-1">
{allowAddSegment ? (
<Button
size="sm"
variant="ghost"
className="pr pl-0.5"
onClick={() => onSegmentAdd(segmentationIdToUse)}
onClick={() => onSegmentAdd(segmentationId)}
>
<Icons.Add />
Add Segment
@@ -68,10 +64,7 @@ export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, s
size="icon"
variant="ghost"
onClick={() =>
onToggleSegmentationRepresentationVisibility(
segmentationIdToUse,
activeRepresentation?.type
)
onToggleSegmentationRepresentationVisibility(segmentationId, representation?.type)
}
>
{Icon}
@@ -1,7 +1,7 @@
import React from 'react';
import { Icons } from '../Icons';
import { useTranslation } from 'react-i18next';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useSegmentationTableContext } from './contexts';
export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
children = null,
@@ -9,7 +9,7 @@ export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
const { t } = useTranslation('SegmentationTable');
const { onSegmentationAdd, data, disableEditing, mode, disabled } =
useSegmentationTableContext('SegmentationTable');
useSegmentationTableContext('AddSegmentationRow');
const isEmpty = data.length === 0;
@@ -0,0 +1,104 @@
import React from 'react';
import { SegmentStatisticsProvider, useSegmentStatistics } from './contexts';
import { roundNumber } from '../../utils';
// Default statistics component
const DefaultStatsList = () => {
const { namedStats } = useSegmentStatistics('DefaultStatsList');
if (!namedStats) {
return null;
}
const handleNumber = (value: number) => {
if (value === null) {
return '';
}
if (Array.isArray(value)) {
return value.map(handleNumber).join(', ');
}
return roundNumber(value);
};
// Sort namedStats entries by order property
const sortedStats = Object.entries(namedStats)
.filter(([_, stat]) => stat && stat.value !== null)
.sort((a, b) => {
const orderA = a[1]?.order ?? Number.MAX_SAFE_INTEGER;
const orderB = b[1]?.order ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
return (
<div className="space-y-1">
{sortedStats.map(([key, stat]) => {
const { label, value, unit } = stat;
return (
<div
key={key}
className="flex justify-between"
>
<div>{label}</div>
<div>
<span className="text-white">{handleNumber(value)}</span>{' '}
<span className="">{unit && unit !== 'none' ? unit : ''}</span>
</div>
</div>
);
})}
</div>
);
};
// Root component that serves as the container and context provider
const SegmentStatisticsRoot = ({ segment, segmentationId, children }) => {
if (!segment) {
return null;
}
return (
<SegmentStatisticsProvider
segment={segment}
segmentationId={segmentationId}
>
<div className="segment-statistics w-full">{children}</div>
</SegmentStatisticsProvider>
);
};
const SegmentStatisticsTitle = ({ children = null }: { children?: React.ReactNode }) => {
return <div className="mb-2">{children}</div>;
};
const SegmentStatisticsHeader = ({ children = null }: { children?: React.ReactNode }) => {
const { segment, segmentationId } = useSegmentStatistics('SegmentStatisticsHeader');
const { segmentIndex } = segment;
return (
<div className="mb-3">
{children &&
React.cloneElement(children as React.ReactElement, { segmentationId, segmentIndex })}
</div>
);
};
const SegmentStatisticsBody = ({ children = null }: { children?: React.ReactNode }) => {
return <div className="segment-statistics-body">{children || <DefaultStatsList />}</div>;
};
const SegmentStatisticsFooter = ({ children = null }: { children?: React.ReactNode }) => {
return <div className="mt-3">{children}</div>;
};
// Create a compound component structure
const SegmentStatistics = Object.assign(SegmentStatisticsRoot, {
Title: SegmentStatisticsTitle,
Header: SegmentStatisticsHeader,
Body: SegmentStatisticsBody,
Footer: SegmentStatisticsFooter,
});
export { SegmentStatistics };
export { useSegmentStatistics };
@@ -1,7 +1,165 @@
import React from 'react';
import { PanelSection } from '../PanelSection';
import { useSegmentationTableContext, SegmentationExpandedProvider } from './contexts';
import { useTranslation } from 'react-i18next';
import {
Button,
Icons,
DropdownMenu,
DropdownMenuTrigger,
Tooltip,
TooltipTrigger,
TooltipContent,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '../../components';
export const SegmentationCollapsed: React.FC<{ children?: React.ReactNode }> = ({
// Main header component
const SegmentationCollapsedHeader = ({ children }: { children: React.ReactNode }) => {
return (
<div className="bg-primary-dark flex h-10 w-full items-center space-x-1 rounded-t px-1.5">
{children}
</div>
);
};
// Dropdown menu component - specifically for dropdown menu content
const SegmentationCollapsedDropdownMenu = ({ children }: { children: React.ReactNode }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.More className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
{children}
</DropdownMenu>
);
};
// Selector component - for the segmentation selection dropdown
const SegmentationCollapsedSelector = () => {
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
const { data, activeSegmentationId, onSegmentationClick } = useSegmentationTableContext(
'SegmentationCollapsedSelector'
);
if (!data?.length) {
return null;
}
const segmentations = data.map(seg => ({
id: seg.segmentation.segmentationId,
label: seg.segmentation.label,
}));
return (
<Select
onValueChange={value => onSegmentationClick(value)}
value={activeSegmentationId}
>
<SelectTrigger className="w-full overflow-hidden">
<SelectValue placeholder={t('Select a segmentation')} />
</SelectTrigger>
<SelectContent>
{segmentations.map(seg => (
<SelectItem
key={seg.id}
value={seg.id}
>
{seg.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
// Info component - for displaying the info tooltip
const SegmentationCollapsedInfo = () => {
const { data, activeSegmentationId } = useSegmentationTableContext('SegmentationCollapsedInfo');
const activeSegmentationObj = data.find(
seg => seg.segmentation.segmentationId === activeSegmentationId
);
const info = activeSegmentationObj?.segmentation.cachedStats?.info;
return (
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.Info className="h-6 w-6" />
</Button>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="end"
>
{info}
</TooltipContent>
</Tooltip>
);
};
// Content component - for the main collapsed view content
const SegmentationCollapsedContent = ({ children }: { children: React.ReactNode }) => {
return <div className="collapsed-content">{children}</div>;
};
// Main compound component
const SegmentationCollapsedRoot: React.FC<{ children?: React.ReactNode }> = ({
children = null,
}) => {
return <div>{children}</div>;
const { mode, data, activeSegmentationId } = 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
);
if (!activeSegmentationInfo) {
return null;
}
return (
<div className="space-y-0">
<PanelSection className="mb-0">
<SegmentationExpandedProvider
segmentation={activeSegmentationInfo.segmentation}
representation={activeSegmentationInfo.representation}
isActive={true}
onSegmentationClick={() => {}} // No-op since it's already the active one
>
{children}
</SegmentationExpandedProvider>
</PanelSection>
</div>
);
};
// Add the subcomponents to the main component
const SegmentationCollapsed = Object.assign(SegmentationCollapsedRoot, {
Header: SegmentationCollapsedHeader,
DropdownMenu: SegmentationCollapsedDropdownMenu,
Selector: SegmentationCollapsedSelector,
Info: SegmentationCollapsedInfo,
Content: SegmentationCollapsedContent,
});
// Export the component
export { SegmentationCollapsed };
@@ -1,47 +1,132 @@
import React from 'react';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { PanelSection } from '../PanelSection';
import { SegmentationHeader } from './SegmentationHeader';
import { SegmentationTable } from './SegmentationTable';
import {
useSegmentationTableContext,
SegmentationExpandedProvider,
useSegmentationExpanded,
} from './contexts';
import { Button } from '../Button';
import { Icons } from '../Icons/Icons';
import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu';
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip';
export const SegmentationExpanded: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { data } = useSegmentationTableContext('SegmentationExpanded');
// Separate the Header component from other children
const headerComponent = React.Children.toArray(children).find(
child => React.isValidElement(child) && child.type === SegmentationTable.Header
);
const otherChildren = React.Children.toArray(children).filter(
child => !(React.isValidElement(child) && child.type === SegmentationTable.Header)
);
// The Header container component
const SegmentationExpandedHeader = ({ children }: { children: React.ReactNode }) => {
const { segmentation, isActive } = useSegmentationExpanded('SegmentationExpandedHeader');
const { onSegmentationClick } = useSegmentationTableContext('SegmentationExpandedHeader');
return (
<>
{data.map(segmentationInfo => (
<PanelSection key={segmentationInfo.segmentation.segmentationId}>
<PanelSection.Header className="border-input border-t-2 bg-transparent pl-1">
{headerComponent ? (
React.cloneElement(headerComponent as React.ReactElement, {
segmentation: segmentationInfo.segmentation,
representation: segmentationInfo.representation,
})
) : (
<SegmentationHeader segmentation={segmentationInfo.segmentation} />
)}
</PanelSection.Header>
<PanelSection.Content>
<div className="segmentation-expanded-section">
{React.Children.map(otherChildren, child =>
React.isValidElement(child)
? React.cloneElement(child, {
segmentation: segmentationInfo.segmentation,
})
: child
)}
</div>
</PanelSection.Content>
</PanelSection>
))}
</>
<PanelSection.Header
className={`border-input border-t-2 bg-transparent pl-1 ${isActive ? 'border-primary' : ''} py-0`}
onClick={e => {
e.stopPropagation();
onSegmentationClick(segmentation.segmentationId);
}}
>
<div className="text-foreground flex h-8 w-full items-center">{children}</div>
</PanelSection.Header>
);
};
// Dropdown menu component - specifically for dropdown menu content
const SegmentationExpandedDropdownMenu = ({ children }: { children: React.ReactNode }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-1"
onClick={e => e.stopPropagation()}
>
<Icons.More />
</Button>
</DropdownMenuTrigger>
{children}
</DropdownMenu>
);
};
// Label component - for displaying the segmentation label
const SegmentationExpandedLabel = () => {
const { segmentation } = useSegmentationExpanded('SegmentationExpandedLabel');
return <div className="pl-1.5">{segmentation.label}</div>;
};
// Info component - for the info tooltip
const SegmentationExpandedInfo = () => {
const { segmentation } = useSegmentationExpanded('SegmentationExpandedInfo');
return (
<div className="ml-auto mr-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.Info className="h-6 w-6" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{segmentation.cachedStats?.info}</p>
</TooltipContent>
</Tooltip>
</div>
);
};
// Content component
const SegmentationExpandedContent = ({ children }: { children: React.ReactNode }) => {
return (
<PanelSection.Content className="py-0">
<div className="segmentation-expanded-section">{children}</div>
</PanelSection.Content>
);
};
// Main compound component
const SegmentationExpandedRoot = ({ children }) => {
const { data, activeSegmentationId, onSegmentationClick, mode } =
useSegmentationTableContext('SegmentationExpanded');
// Check if we should render based on mode
if (mode !== 'expanded' || !data || data.length === 0) {
return null;
}
return (
<div className="space-y-0">
{data.map(segmentationInfo => {
const isActive = segmentationInfo.segmentation.segmentationId === activeSegmentationId;
return (
<PanelSection
key={segmentationInfo.segmentation.segmentationId}
className="mb-0"
>
<SegmentationExpandedProvider
segmentation={segmentationInfo.segmentation}
representation={segmentationInfo.representation}
isActive={isActive}
onSegmentationClick={onSegmentationClick}
>
{children}
</SegmentationExpandedProvider>
</PanelSection>
);
})}
</div>
);
};
const SegmentationExpanded = Object.assign(SegmentationExpandedRoot, {
Header: SegmentationExpandedHeader,
DropdownMenu: SegmentationExpandedDropdownMenu,
Label: SegmentationExpandedLabel,
Info: SegmentationExpandedInfo,
Content: SegmentationExpandedContent,
});
export { SegmentationExpanded };
@@ -3,29 +3,23 @@ import { Button } from '../Button';
import { Icons } from '../Icons/Icons';
import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu';
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useTranslation } from 'react-i18next';
import { useSegmentationExpanded, useSegmentationTableContext } from './contexts';
// Then use it in your component:
export const SegmentationHeader: React.FC<{
children?: React.ReactNode;
segmentation?: any;
}> = ({ children, segmentation }) => {
const { t } = useTranslation('SegmentationTable');
const { ...contextProps } = useSegmentationTableContext('SegmentationHeader');
}> = ({ children }) => {
// Always call both hooks unconditionally at the top level
const expandedContext = useSegmentationExpanded('SegmentationHeader');
const tableContext = useSegmentationTableContext('SegmentationHeader');
// Determine which segmentation to use
const segmentation = expandedContext?.segmentation || tableContext.activeSegmentation;
if (!segmentation) {
return null;
}
const childrenWithProps = React.Children.map(children, child =>
React.isValidElement(child)
? React.cloneElement(child, {
t,
...contextProps,
})
: child
);
return (
<div className="text-foreground flex h-8 w-full items-center justify-between">
<div className="flex items-center space-x-1">
@@ -40,7 +34,7 @@ export const SegmentationHeader: React.FC<{
<Icons.More />
</Button>
</DropdownMenuTrigger>
{childrenWithProps}
{children}
</DropdownMenu>
<div className="pl-1.5">{segmentation.label}</div>
</div>
@@ -55,7 +49,7 @@ export const SegmentationHeader: React.FC<{
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{segmentation.cachedStats.info}</p>
<p>{segmentation.cachedStats?.info}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -1,11 +1,10 @@
import React from 'react';
import { ScrollArea, DataRow } from '../../components';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { HoverCard, HoverCardTrigger, HoverCardContent } from '../../components/HoverCard';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
import { SegmentStatistics } from './SegmentStatistics';
export const SegmentationSegments: React.FC<{
segmentation?: unknown;
representation?: unknown;
}> = ({ segmentation, representation }) => {
export const SegmentationSegments = ({ children = null }: { children?: React.ReactNode }) => {
const {
activeSegmentationId,
disableEditing,
@@ -13,37 +12,51 @@ export const SegmentationSegments: React.FC<{
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentClick,
mode,
onSegmentEdit,
onSegmentDelete,
data,
} = useSegmentationTableContext('SegmentationTable.Segments');
} = useSegmentationTableContext('SegmentationSegments');
let segmentationToUse = segmentation;
let representationToUse = representation;
let segmentationIdToUse = activeSegmentationId;
if (!segmentationToUse || !representationToUse) {
const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId);
segmentationToUse = entry?.segmentation;
representationToUse = entry?.representation;
segmentationIdToUse = entry?.segmentation.segmentationId;
// Try to get segmentation data from expanded context first, then fall back to table context
let segmentation;
let representation;
try {
// Try to use the SegmentationExpanded context if available
const segmentationInfo = useSegmentationExpanded('SegmentationSegments');
segmentation = segmentationInfo.segmentation;
representation = segmentationInfo.representation;
} catch (e) {
// Not within SegmentationExpanded context, get from active segmentation
const segmentationInfo = data.find(
entry => entry.segmentation.segmentationId === activeSegmentationId
);
segmentation = segmentationInfo?.segmentation;
representation = segmentationInfo?.representation;
}
if (!representationToUse || !segmentationToUse) {
if (!representation || !segmentation) {
return null;
}
const segments = Object.values(representation.segments);
const isActiveSegmentation = segmentation.segmentationId === activeSegmentationId;
return (
<ScrollArea
className={`ohif-scrollbar invisible-scrollbar bg-bkg-low space-y-px h-[900px]`}
className={`ohif-scrollbar invisible-scrollbar bg-bkg-low max-h-80 space-y-px`}
showArrows={true}
>
{Object.values(representationToUse.segments).map(segment => {
{segments.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, visible } = segment;
const segmentFromSegmentation = segmentationToUse.segments[segmentIndex];
const { segmentIndex, color, visible } = segment as {
segmentIndex: number;
color: number[];
visible: boolean;
};
const segmentFromSegmentation = segmentation.segments[segmentIndex];
if (!segmentFromSegmentation) {
return null;
@@ -53,27 +66,70 @@ export const SegmentationSegments: React.FC<{
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<DataRow
key={segmentIndex}
number={segmentIndex}
title={label}
details={displayText}
colorHex={cssColor}
isSelected={active}
isVisible={visible}
isLocked={locked}
disableEditing={disableEditing}
onColor={() => onSegmentColorClick(segmentationIdToUse, segmentIndex)}
onToggleVisibility={() =>
onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type)
}
onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)}
onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)}
onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)}
onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)}
/>
<HoverCard
key={`hover-${segmentIndex}`}
openDelay={300}
>
<HoverCardTrigger asChild>
{/* The div here is not random, it is needed for making it work with the hover card */}
<div>
<DataRow
key={segmentIndex}
number={segmentIndex}
title={label}
details={displayText}
description={displayText}
colorHex={cssColor}
isSelected={active}
isVisible={visible}
isLocked={locked}
disableEditing={disableEditing}
className={!isActiveSegmentation ? 'opacity-80' : ''}
onColor={() => onSegmentColorClick(segmentation.segmentationId, segmentIndex)}
onToggleVisibility={() =>
onToggleSegmentVisibility(
segmentation.segmentationId,
segmentIndex,
representation.type
)
}
onToggleLocked={() =>
onToggleSegmentLock(segmentation.segmentationId, segmentIndex)
}
onSelect={() => onSegmentClick(segmentation.segmentationId, segmentIndex)}
onRename={() => onSegmentEdit(segmentation.segmentationId, segmentIndex)}
onDelete={() => onSegmentDelete(segmentation.segmentationId, segmentIndex)}
/>
</div>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className="w-72 border"
>
<div className="mb-4 flex items-center space-x-2">
<div
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: cssColor }}
></div>
<h3 className="text-muted-foreground break-words font-semibold">{label}</h3>
</div>
<SegmentStatistics
segment={{
...segmentFromSegmentation,
segmentIndex,
}}
segmentationId={segmentation.segmentationId}
>
{children}
</SegmentStatistics>
</HoverCardContent>
</HoverCard>
);
})}
</ScrollArea>
);
};
SegmentationSegments.displayName = 'SegmentationTable.Segments';
@@ -1,108 +0,0 @@
import React from 'react';
import {
Icons,
Button,
DropdownMenu,
DropdownMenuTrigger,
Tooltip,
TooltipTrigger,
TooltipContent,
} from '../../components';
import { useTranslation } from 'react-i18next';
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@ohif/ui-next';
import { useSegmentationTableContext } from './SegmentationTableContext';
export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }> = ({
children = null,
}) => {
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
const { data, activeSegmentationId, mode, onSegmentationClick, exportOptions, ...contextProps } =
useSegmentationTableContext('SegmentationTable.HeaderCollapsed');
if (mode !== 'collapsed' || !data?.length) {
return null;
}
const activeSegmentationObj = data.find(
seg => seg.segmentation.segmentationId === activeSegmentationId
);
const activeSegmentation = {
id: activeSegmentationObj?.segmentation.segmentationId,
label: activeSegmentationObj?.segmentation.label,
info: activeSegmentationObj?.segmentation.cachedStats?.info,
};
const segmentations = data.map(seg => ({
id: seg.segmentation.segmentationId,
label: seg.segmentation.label,
info: seg.segmentation.cachedStats?.info,
}));
const allowExport = exportOptions?.find(
({ segmentationId }) => segmentationId === activeSegmentation.id
)?.isExportable;
const childrenWithProps = React.Children.map(children, child =>
React.isValidElement(child)
? React.cloneElement(child, {
activeSegmentation,
allowExport,
t,
...contextProps,
})
: child
);
return (
<div className="bg-primary-dark flex h-10 w-full items-center space-x-1 rounded-t px-1.5">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.More className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
{childrenWithProps}
</DropdownMenu>
<Select
onValueChange={value => onSegmentationClick(value)}
value={activeSegmentation.id}
>
<SelectTrigger className="w-full overflow-hidden">
<SelectValue placeholder={t('Select a segmentation')} />
</SelectTrigger>
<SelectContent>
{segmentations.map(seg => (
<SelectItem
key={seg.id}
value={seg.id}
>
{seg.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
>
<Icons.Info className="h-6 w-6" />
</Button>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="end"
>
{activeSegmentation.info}
</TooltipContent>
</Tooltip>
</div>
);
};
@@ -1,20 +1,26 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useState, Children, isValidElement } from 'react';
import { useTranslation } from 'react-i18next';
import { PanelSection } from '../PanelSection';
import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext';
import {
SegmentationTableProvider,
SegmentationTableContextType,
ViewportSegmentationInfo,
} from './contexts';
import { SegmentationSegments } from './SegmentationSegments';
import { SegmentStatistics } from './SegmentStatistics';
import { SegmentationTableConfig } from './SegmentationTableConfig';
import { AddSegmentRow } from './AddSegmentRow';
import { AddSegmentationRow } from './AddSegmentationRow';
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
import { SegmentationHeader } from './SegmentationHeader';
import { SegmentationCollapsed } from './SegmentationCollapsed';
import { SegmentationExpanded } from './SegmentationExpanded';
import Icons from '../Icons';
interface SegmentationTableProps extends SegmentationTableContext {
disabled?: boolean;
// Only include props that aren't part of the context
interface SegmentationTableProps extends Omit<SegmentationTableContextType, 'setShowConfig'> {
title?: string;
children?: ReactNode;
setShowConfig?: (value: boolean) => void;
}
interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
@@ -22,55 +28,118 @@ interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
Config: typeof SegmentationTableConfig;
AddSegmentRow: typeof AddSegmentRow;
AddSegmentationRow: typeof AddSegmentationRow;
SelectorHeader: typeof SegmentationSelectorHeader;
Header: typeof SegmentationHeader;
Collapsed: typeof SegmentationCollapsed;
Expanded: typeof SegmentationExpanded;
SegmentStatistics: typeof SegmentStatistics;
}
export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => {
export const SegmentationTableRoot = (props: SegmentationTableProps) => {
const { t } = useTranslation('SegmentationTable');
const { data = [], mode, title, disableEditing, disabled, children, ...contextProps } = props;
const {
data = [],
mode,
title,
disableEditing = false,
disabled = false,
children,
showConfig: externalShowConfig,
...contextProps
} = props;
const [internalShowConfig, setInternalShowConfig] = useState(false);
const showConfig = externalShowConfig !== undefined ? externalShowConfig : internalShowConfig;
// Find the active segmentation info based on which representation is active
const activeSegmentationInfo = data.find(info => info.representation?.active);
const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId;
const activeRepresentation = activeSegmentationInfo?.representation;
const activeSegmentation = activeSegmentationInfo?.segmentation;
const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } =
activeRepresentation?.styles ?? {};
// Get the active segmentation ID
const activeSegmentationId =
props.activeSegmentationId || activeSegmentationInfo?.segmentation?.segmentationId;
const activeRepresentation = props.activeRepresentation || activeSegmentationInfo?.representation;
const activeSegmentation = props.activeSegmentation || activeSegmentationInfo?.segmentation;
// Extract style properties or use defaults
const {
fillAlpha = props.fillAlpha || 0.5,
fillAlphaInactive = props.fillAlphaInactive || 0.2,
outlineWidth = props.outlineWidth || 1,
renderFill = props.renderFill !== undefined ? props.renderFill : true,
renderOutline = props.renderOutline !== undefined ? props.renderOutline : true,
} = activeRepresentation?.styles ?? {};
// Check if SegmentationTableConfig is present in children
const hasConfigComponent = Children.toArray(children).some(
child => isValidElement(child) && child.type === SegmentationTableConfig
);
// Process children to conditionally render the config component based on showConfig
const processedChildren = Children.map(children, child => {
if (isValidElement(child) && child.type === SegmentationTableConfig) {
// Only render the Config component if showConfig is true
return showConfig ? child : null;
}
return child;
});
const toggleShowConfig = () => {
if (props.setShowConfig) {
props.setShowConfig(!showConfig);
} else {
setInternalShowConfig(!internalShowConfig);
}
};
return (
<SegmentationTableProvider
data={data}
mode={mode}
disabled={disabled}
disableEditing={disableEditing}
fillAlpha={fillAlpha}
fillAlphaInactive={fillAlphaInactive}
outlineWidth={outlineWidth}
renderFill={renderFill}
renderOutline={renderOutline}
activeSegmentationId={activeSegmentationId}
activeSegmentation={activeSegmentation}
activeRepresentation={activeRepresentation}
{...contextProps}
value={{
data,
mode,
showConfig,
disabled,
disableEditing,
fillAlpha,
fillAlphaInactive,
outlineWidth,
renderFill,
renderOutline,
activeSegmentationId,
activeSegmentation,
activeRepresentation,
...contextProps,
setShowConfig: toggleShowConfig,
}}
>
<PanelSection defaultOpen={true}>
<PanelSection.Header>
<PanelSection.Header className="flex items-center justify-between">
<span>{t(title)}</span>
{hasConfigComponent && (
<div className="ml-auto mr-2">
<Icons.Settings
className="text-primary-active h-4 w-4"
onClick={e => {
e.stopPropagation();
toggleShowConfig();
}}
/>
</div>
)}
</PanelSection.Header>
<PanelSection.Content>{children}</PanelSection.Content>
<PanelSection.Content>{processedChildren}</PanelSection.Content>
</PanelSection>
</SegmentationTableProvider>
);
};
SegmentationTable.Segments = SegmentationSegments;
SegmentationTable.Config = SegmentationTableConfig;
SegmentationTable.AddSegmentRow = AddSegmentRow;
SegmentationTable.AddSegmentationRow = AddSegmentationRow;
SegmentationTable.SelectorHeader = SegmentationSelectorHeader;
SegmentationTable.Header = SegmentationHeader;
SegmentationTable.Collapsed = SegmentationCollapsed;
SegmentationTable.Expanded = SegmentationExpanded;
const SegmentationTable = Object.assign(SegmentationTableRoot, {
Segments: SegmentationSegments,
Config: SegmentationTableConfig,
AddSegmentRow: AddSegmentRow,
AddSegmentationRow: AddSegmentationRow,
Collapsed: SegmentationCollapsed,
Expanded: SegmentationExpanded,
SegmentStatistics: SegmentStatistics,
Header: SegmentationHeader,
}) as SegmentationTableComponent;
export { SegmentationTable };
@@ -1,13 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { PanelSection } from '../PanelSection';
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
import { Slider } from '../Slider';
import { Icons } from '../Icons';
import { Switch } from '../Switch';
import { Label } from '../Label';
import { Input } from '../Input';
import { useSegmentationTableContext } from './SegmentationTableContext';
import { useSegmentationTableContext } from './contexts';
export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const { t } = useTranslation('SegmentationTable.AppearanceSettings');
@@ -26,152 +25,134 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> =
renderInactiveSegmentations,
toggleRenderInactiveSegmentations,
data,
} = useSegmentationTableContext('styles');
} = useSegmentationTableContext('SegmentationTableConfig');
if (!data?.length) {
return null;
}
return (
<PanelSection defaultOpen={false}>
<PanelSection.Header>
<div className="flex items-center">
<Icons.Settings className="mr-2 h-4 w-4" />
<span>{t('Appearance Settings')}</span>
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-1.5 pt-0.5 pb-3">
<div className="my-1 flex items-center justify-between">
<span className="text-aqua-pale text-xs">
{t('Show')}:{' '}
{renderFill && renderOutline
? t('Fill & Outline')
: renderOutline
? t('Outline Only')
: t('Fill Only')}
</span>
<Tabs
value={
renderFill && renderOutline ? 'fill-and-outline' : renderOutline ? 'outline' : 'fill'
}
onValueChange={value => {
if (value === 'fill-and-outline') {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, true);
} else if (value === 'outline') {
setRenderFill({ type: activeRepresentation.type }, false);
setRenderOutline({ type: activeRepresentation.type }, true);
} else {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, false);
}
}}
>
<TabsList>
<TabsTrigger value="fill-and-outline">
<Icons.FillAndOutline className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="outline">
<Icons.OutlineOnly className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="fill">
<Icons.FillOnly className="text-primary-active" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="space-y-2">
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlpha]}
onValueChange={([value]) => setFillAlpha({ type: activeRepresentation.type }, value)}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlpha}
onChange={e =>
setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
</PanelSection.Header>
<PanelSection.Content>
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-1.5 pt-0.5 pb-3">
<div className="my-1 flex items-center justify-between">
<span className="text-aqua-pale text-xs">
{t('Show')}:{' '}
{renderFill && renderOutline
? t('Fill & Outline')
: renderOutline
? t('Outline Only')
: t('Fill Only')}
</span>
<Tabs
value={
renderFill && renderOutline
? 'fill-and-outline'
: renderOutline
? 'outline'
: 'fill'
}
onValueChange={value => {
if (value === 'fill-and-outline') {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, true);
} else if (value === 'outline') {
setRenderFill({ type: activeRepresentation.type }, false);
setRenderOutline({ type: activeRepresentation.type }, true);
} else {
setRenderFill({ type: activeRepresentation.type }, true);
setRenderOutline({ type: activeRepresentation.type }, false);
}
}}
>
<TabsList>
<TabsTrigger value="fill-and-outline">
<Icons.FillAndOutline className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="outline">
<Icons.OutlineOnly className="text-primary-active" />
</TabsTrigger>
<TabsTrigger value="fill">
<Icons.FillOnly className="text-primary-active" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="space-y-2">
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlpha]}
onValueChange={([value]) =>
setFillAlpha({ type: activeRepresentation.type }, value)
}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlpha}
onChange={e =>
setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
{t('Border')}
</Label>
<Slider
value={[outlineWidth]}
onValueChange={([value]) =>
setOutlineWidth({ type: activeRepresentation.type }, value)
}
max={10}
min={0}
step={0.1}
className="mx-1 flex-1"
/>
<Input
value={outlineWidth}
onChange={e =>
setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value))
}
className="mx-1 w-10 flex-none text-center"
/>
</div>
</div>
<div className="border-input w-full border"></div>
<div className="my-2 flex items-center pl-1">
<Switch
checked={renderInactiveSegmentations}
onCheckedChange={toggleRenderInactiveSegmentations}
/>
<Label className="text-muted-foreground mx-2 text-xs">
{t('Display inactive segmentations')}
</Label>
</div>
{renderInactiveSegmentations && (
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlphaInactive]}
onValueChange={([value]) =>
setFillAlphaInactive({ type: activeRepresentation.type }, value)
}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlphaInactive}
onChange={e =>
setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
)}
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
{t('Border')}
</Label>
<Slider
value={[outlineWidth]}
onValueChange={([value]) => setOutlineWidth({ type: activeRepresentation.type }, value)}
max={10}
min={0}
step={0.1}
className="mx-1 flex-1"
/>
<Input
value={outlineWidth}
onChange={e =>
setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value))
}
className="mx-1 w-10 flex-none text-center"
/>
</div>
{children}
</PanelSection.Content>
</PanelSection>
</div>
<div className="border-input w-full border"></div>
<div className="my-2 flex items-center pl-1">
<Switch
checked={renderInactiveSegmentations}
onCheckedChange={toggleRenderInactiveSegmentations}
/>
<Label className="text-muted-foreground mx-2 text-xs">
{t('Display inactive segmentations')}
</Label>
</div>
{renderInactiveSegmentations && (
<div className="my-2 flex items-center">
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
Opacity
</Label>
<Slider
className="mx-1 flex-1"
value={[fillAlphaInactive]}
onValueChange={([value]) =>
setFillAlphaInactive({ type: activeRepresentation.type }, value)
}
max={1}
min={0}
step={0.1}
/>
<Input
className="mx-1 w-10 flex-none"
value={fillAlphaInactive}
onChange={e =>
setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value))
}
/>
</div>
)}
{children}
</div>
);
};
@@ -1,80 +0,0 @@
import { createContext } from '../../lib/createContext';
interface Segmentation {
segmentationId: string;
label: string;
cachedStats: {
info: string;
};
}
interface Representation {
active: boolean;
visible: boolean;
type: string;
styles: {
fillAlpha: number;
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
};
}
interface ViewportSegmentationInfo {
segmentation: Segmentation;
representation: Representation;
}
interface SegmentationTableContext {
data: ViewportSegmentationInfo[];
disabled: boolean;
mode: 'collapsed' | 'expanded';
fillAlpha: number;
exportOptions: {
segmentationId: string;
isExportable: boolean;
}[];
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
activeSegmentationId: string;
activeRepresentation: Representation;
activeSegmentation: Segmentation;
setRenderFill: ({ type }, value: boolean) => void;
setRenderOutline: ({ type }, value: boolean) => void;
setOutlineWidth: ({ type }, value: number) => void;
setFillAlpha: ({ type }, value: number) => void;
setFillAlphaInactive: ({ type }, value: number) => void;
renderInactiveSegmentations: boolean;
toggleRenderInactiveSegmentations: () => void;
onSegmentationAdd: (segmentationId: 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;
onSegmentationEdit: (segmentationId: string) => void;
onSegmentColorClick: (segmentationId: string, segmentIndex: number) => void;
onSegmentDelete: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentVisibility: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentLock: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentationRepresentationVisibility: (segmentationId: string, type: string) => void;
onSegmentationDownload: (segmentationId: string) => void;
storeSegmentation: (segmentationId: string) => void;
onSegmentationDownloadRTSS: (segmentationId: string) => void;
setStyle: (
segmentationId: string,
representationType: string,
styleKey: string,
value: unknown
) => void;
onSegmentationRemoveFromViewport: (segmentationId: string) => void;
disableEditing: boolean;
}
const [SegmentationTableProvider, useSegmentationTableContext] =
createContext<SegmentationTableContext>('SegmentationTable');
export { SegmentationTableProvider, useSegmentationTableContext, SegmentationTableContext };
@@ -0,0 +1,45 @@
import React, { createContext, useContext } from 'react';
interface SegmentStatisticsContextType {
segment: {
segmentIndex: number;
cachedStats: any;
};
namedStats: Record<string, any> | undefined;
segmentationId: string;
}
// Create a named context
const SegmentStatisticsContext = createContext<SegmentStatisticsContextType | undefined>(undefined);
SegmentStatisticsContext.displayName = 'SegmentStatisticsContext';
export const useSegmentStatistics = (componentName?: string) => {
const context = useContext(SegmentStatisticsContext);
if (context === undefined) {
throw new Error(
`useSegmentStatistics must be used within a SegmentStatisticsProvider` +
(componentName ? ` (called from ${componentName})` : '')
);
}
return context;
};
export const SegmentStatisticsProvider: React.FC<{
segment: {
segmentIndex: number;
cachedStats: any;
};
segmentationId: string;
children: React.ReactNode;
}> = ({ segment, segmentationId, children }) => {
const { cachedStats } = segment || {};
const { namedStats } = cachedStats || {};
return (
<SegmentStatisticsContext.Provider value={{ segment, namedStats, segmentationId }}>
{children}
</SegmentStatisticsContext.Provider>
);
};
@@ -0,0 +1,40 @@
import React, { createContext, useContext } from 'react';
// Context for expanded segmentation panel
interface SegmentationExpandedContextType {
segmentation: any;
representation: any;
isActive: boolean;
}
// Create a named context
const SegmentationExpandedContext = createContext<SegmentationExpandedContextType | undefined>(
undefined
);
SegmentationExpandedContext.displayName = 'SegmentationExpandedContext';
export const useSegmentationExpanded = (componentName?: string) => {
const context = useContext(SegmentationExpandedContext);
if (context === undefined) {
throw new Error(
`useSegmentationExpanded must be used within a SegmentationExpandedProvider` +
(componentName ? ` (called from ${componentName})` : '')
);
}
return context;
};
export const SegmentationExpandedProvider: React.FC<{
segmentation: any;
representation: any;
isActive: boolean;
children: React.ReactNode;
}> = ({ segmentation, representation, isActive, children }) => {
return (
<SegmentationExpandedContext.Provider value={{ segmentation, representation, isActive }}>
{children}
</SegmentationExpandedContext.Provider>
);
};
@@ -0,0 +1,129 @@
import React, { createContext, useContext } from 'react';
export interface SegmentStats {
info: string;
namedStats?: Record<string, number | string>;
}
export interface Segment {
color?: string;
label: string;
segmentIndex: number;
visible?: boolean;
locked?: boolean;
}
export interface Segmentation {
segmentationId: string;
label: string;
cachedStats: SegmentStats;
segments: Record<number, Segment>;
}
export interface RepresentationStyles {
fillAlpha: number;
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
}
export interface Representation {
active: boolean;
visible: boolean;
type: string;
segments: Record<number, Segment>;
styles: RepresentationStyles;
}
export interface ViewportSegmentationInfo {
segmentation: Segmentation;
representation: Representation;
}
export interface ExportOption {
segmentationId: string;
isExportable: boolean;
}
export interface SegmentationTableContextType {
data: ViewportSegmentationInfo[];
disabled: boolean;
mode: 'collapsed' | 'expanded';
fillAlpha: number;
exportOptions?: ExportOption[];
showConfig: boolean;
fillAlphaInactive: number;
outlineWidth: number;
renderFill: boolean;
renderOutline: boolean;
activeSegmentationId?: string;
activeRepresentation?: Representation;
activeSegmentation?: Segmentation;
disableEditing: boolean;
showAddSegment?: boolean;
renderInactiveSegmentations?: boolean;
// Function handlers
setShowConfig?: (show: boolean) => void;
setRenderFill?: ({ type }: { type: string }, value: boolean) => void;
setRenderOutline?: ({ type }: { type: string }, value: boolean) => void;
setOutlineWidth?: ({ type }: { type: string }, value: number) => void;
setFillAlpha?: ({ type }: { type: string }, value: number) => void;
setFillAlphaInactive?: ({ type }: { type: string }, value: number) => void;
toggleRenderInactiveSegmentations?: () => void;
onSegmentationAdd?: (segmentationId: 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;
onSegmentationEdit?: (segmentationId: string) => void;
onSegmentColorClick?: (segmentationId: string, segmentIndex: number) => void;
onSegmentDelete?: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentVisibility?: (
segmentationId: string,
segmentIndex: number,
representationType?: string
) => void;
onToggleSegmentLock?: (segmentationId: string, segmentIndex: number) => void;
onToggleSegmentationRepresentationVisibility?: (segmentationId: string, type: string) => void;
onSegmentationDownload?: (segmentationId: string) => void;
storeSegmentation?: (segmentationId: string) => void;
onSegmentationDownloadRTSS?: (segmentationId: string) => void;
setStyle?: (
segmentationId: string,
representationType: string,
styleKey: string,
value: unknown
) => void;
onSegmentationRemoveFromViewport?: (segmentationId: string) => void;
}
const SegmentationTableContext = createContext<SegmentationTableContextType | undefined>(undefined);
SegmentationTableContext.displayName = 'SegmentationTableContext';
export const SegmentationTableProvider = ({
value,
children,
}: {
value: SegmentationTableContextType;
children: React.ReactNode;
}) => {
return (
<SegmentationTableContext.Provider value={value}>{children}</SegmentationTableContext.Provider>
);
};
export const useSegmentationTableContext = (componentName?: string) => {
const context = useContext(SegmentationTableContext);
if (context === undefined) {
throw new Error(
`useSegmentationTableContext must be used within a SegmentationTableProvider` +
(componentName ? ` (called from ${componentName})` : '')
);
}
return context;
};
@@ -0,0 +1,15 @@
import { useSegmentationTableContext, SegmentationTableProvider } from './SegmentationTableContext';
import {
useSegmentationExpanded,
SegmentationExpandedProvider,
} from './SegmentationExpandedContext';
import { useSegmentStatistics, SegmentStatisticsProvider } from './SegmentStatisticsContext';
export {
useSegmentationTableContext,
useSegmentationExpanded,
useSegmentStatistics,
SegmentationTableProvider,
SegmentationExpandedProvider,
SegmentStatisticsProvider,
};
@@ -3,20 +3,22 @@ import { SegmentationTableConfig } from './SegmentationTableConfig';
import { AddSegmentationRow } from './AddSegmentationRow';
import { AddSegmentRow } from './AddSegmentRow';
import { SegmentationSegments } from './SegmentationSegments';
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
import { SegmentationHeader } from './SegmentationHeader';
import { useSegmentationTableContext } from './SegmentationTableContext';
import {
useSegmentationTableContext,
useSegmentStatistics,
useSegmentationExpanded,
} from './contexts';
SegmentationTable.Segments = SegmentationSegments;
SegmentationTable.Config = SegmentationTableConfig;
SegmentationTable.AddSegmentRow = AddSegmentRow;
SegmentationTable.AddSegmentationRow = AddSegmentationRow;
SegmentationTable.SelectorHeader = SegmentationSelectorHeader;
SegmentationTable.Header = SegmentationHeader;
export {
// context
useSegmentationTableContext,
useSegmentStatistics,
useSegmentationExpanded,
// components
SegmentationTable,
};