feat(segmentation): segment statistics, labelmap interpolation and segment bidirectional (#4865)
This commit is contained in:
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 };
|
||||
+45
@@ -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>
|
||||
);
|
||||
};
|
||||
+40
@@ -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>
|
||||
);
|
||||
};
|
||||
+129
@@ -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,
|
||||
};
|
||||
Reference in new issue
Block a user