feature(segmentation): Add contour tools and utilities in a side panel (#5517)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-10-29 14:15:52 -04:00
1 parent ab440f086a
commit 35bfc9b0cc
73 files changed
+2770 -435

No files matched your search

@@ -28,7 +28,7 @@ const SidePanelWithServices = ({
onClose,
...props
}: SidePanelWithServicesProps) => {
const panelService = servicesManager?.services?.panelService;
const { panelService, toolbarService, viewportGridService } = servicesManager.services;
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
@@ -37,9 +37,15 @@ const SidePanelWithServices = ({
const [closedManually, setClosedManually] = useState(false);
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
setActiveTabIndex(activeTabIndex);
}, []);
const handleActiveTabIndexChange = useCallback(
({ activeTabIndex }) => {
const { activeViewportId: viewportId } = viewportGridService.getState();
toolbarService.refreshToolbarState({ viewportId });
setActiveTabIndex(activeTabIndex);
},
[toolbarService, viewportGridService]
);
const handleOpen = useCallback(() => {
setSidePanelExpanded(true);
+32 -1
View File
@@ -1,9 +1,30 @@
import React from 'react';
import { useToolbar } from '@ohif/core';
/**
* Props for the Toolbar component that renders a collection of toolbar buttons and/or button sections.
*
* @interface ToolbarProps
*/
interface ToolbarProps {
/**
* The section of buttons to display in the toolbar.
* Common values include 'primary', 'secondary', 'tertiary', etc.
* Defaults to 'primary' if not specified.
*
* @default 'primary'
*/
buttonSection?: string;
/**
* The unique identifier of the viewport this toolbar is associated with.
*/
viewportId?: string;
/**
* The numeric position or location of the toolbar.
* Used for ordering and layout purposes in the UI.
*/
location?: number;
}
@@ -60,7 +81,17 @@ export function Toolbar({ buttonSection = 'primary', viewportId, location }: Too
/>
);
return <div key={id}>{tool}</div>;
return (
<div
key={id}
// This wrapper div exists solely for React's key prop requirement during reconciliation.
// We use display:contents to make it transparent to the layout engine (children appear
// as direct children of the parent) while keeping it in the DOM for React's virtual DOM.
className="contents"
>
{tool}
</div>
);
})}
</>
);
@@ -10,6 +10,7 @@ import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper';
import ToolRowWrapper from './Toolbar/ToolRowWrapper';
import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper';
import { ToolButtonWrapper } from './Toolbar/ToolButtonWrapper';
import { Toolbar } from './Toolbar';
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const { cineService } = servicesManager.services;
@@ -45,6 +46,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
name: 'ohif.progressDropdown',
defaultComponent: ProgressDropdownWithService,
},
{
name: 'ohif.Toolbar',
defaultComponent: Toolbar,
},
{
name: 'evaluate.cine',
evaluate: () => {
+24 -41
View File
@@ -1,12 +1,21 @@
import React, { useState } from 'react';
import { Icons, PanelSection, ToolSettings } from '@ohif/ui-next';
import { useSystem, useToolbar } from '@ohif/core';
import classnames from 'classnames';
import { useSystem, useToolbar, useActiveToolOptions } from '@ohif/core';
import { useTranslation } from 'react-i18next';
interface ButtonProps {
isActive?: boolean;
options?: unknown;
/**
* Props for the Toolbox component that renders a collection of toolbar button sections.
*/
interface ToolboxProps {
/**
* The unique identifier of the button section this toolbox represents.
*/
buttonSectionId: string;
/**
* The display title for the toolbox.
*/
title: string;
}
/**
@@ -19,7 +28,7 @@ interface ButtonProps {
* role in enhancing the app with a toolbox by providing a way to integrate
* and display various tools and their corresponding options
*/
export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; title: string }) {
export function Toolbox({ buttonSectionId, title }: ToolboxProps) {
const { servicesManager } = useSystem();
const { t } = useTranslation();
@@ -30,6 +39,8 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
buttonSection: buttonSectionId,
});
const { activeToolOptions } = useActiveToolOptions({ buttonSectionId });
if (!toolboxSections.length) {
return null;
}
@@ -41,35 +52,6 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
);
}
// Helper to check a list of buttons for an active tool.
const findActiveOptions = (buttons: any[]): unknown => {
for (const tool of buttons) {
if (tool.componentProps.isActive) {
return tool.componentProps.options;
}
if (tool.componentProps.buttonSection) {
const nestedButtons = toolbarService.getButtonPropsInButtonSection(
tool.componentProps.buttonSection
) as ButtonProps[];
const activeNested = nestedButtons.find(nested => nested.isActive);
if (activeNested) {
return activeNested.options;
}
}
}
return null;
};
// Look for active tool options across all sections.
const activeToolOptions = toolboxSections.reduce((activeOptions, section) => {
if (activeOptions) {
return activeOptions;
}
const sectionId = section.componentProps.buttonSection;
const buttons = toolbarService.getButtonSection(sectionId);
return findActiveOptions(buttons);
}, null);
// Define the interaction handler once.
const handleInteraction = ({ itemId }: { itemId: string }) => {
onInteraction?.({ itemId });
@@ -103,19 +85,20 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
return (
<div
key={sectionId}
className="bg-muted flex flex-wrap space-x-2 py-2 px-1"
className="bg-muted flex flex-wrap gap-2 py-2 px-1"
>
{buttons.map(tool => {
if (!tool) {
// Skip over tools that are not visible. The visible flag is typically set to
// false as a result of the evaluator function. The evaluator might explicitly
// set visible to false. Alternatively, the ToolbarService will set the visible flag to
// false when the evaluator sets disabled to true and the tool has the hideWhenDisabled flag set to true.
if (!tool || !tool.componentProps.visible) {
return null;
}
const { id, Component, componentProps } = tool;
return (
<div
key={id}
className={classnames('ml-1')}
>
<div key={id}>
<Component
{...componentProps}
id={id}