-
+
);
diff --git a/extensions/default/src/getToolbarModule.tsx b/extensions/default/src/getToolbarModule.tsx
index 9704acc08..6e9b32d4a 100644
--- a/extensions/default/src/getToolbarModule.tsx
+++ b/extensions/default/src/getToolbarModule.tsx
@@ -1,16 +1,15 @@
-import { ToolButton, utils } from '@ohif/ui-next';
+import { utils } from '@ohif/ui-next';
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
// legacy
-import ToolbarDividerLegacy from './Toolbar/ToolbarDivider';
-import ToolbarSplitButtonWithServicesLegacy from './Toolbar/ToolbarSplitButtonWithServices';
-import ToolbarButtonGroupWithServicesLegacy from './Toolbar/ToolbarButtonGroupWithServices';
import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService';
// new
import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper';
+import ToolRowWrapper from './Toolbar/ToolRowWrapper';
import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper';
+import { ToolButtonWrapper } from './Toolbar/ToolButtonWrapper';
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const { cineService } = servicesManager.services;
@@ -18,12 +17,16 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
// new
{
name: 'ohif.toolButton',
- defaultComponent: ToolButton,
+ defaultComponent: ToolButtonWrapper,
},
{
name: 'ohif.toolButtonList',
defaultComponent: ToolButtonListWrapper,
},
+ {
+ name: 'ohif.row',
+ defaultComponent: ToolRowWrapper,
+ },
{
name: 'ohif.toolBoxButtonGroup',
defaultComponent: ToolBoxButtonGroupWrapper,
diff --git a/extensions/default/src/hooks/usePatientInfo.tsx b/extensions/default/src/hooks/usePatientInfo.tsx
index a28d94dfc..ba7ad4156 100644
--- a/extensions/default/src/hooks/usePatientInfo.tsx
+++ b/extensions/default/src/hooks/usePatientInfo.tsx
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
-import { utils } from '@ohif/core';
+import { utils, useSystem } from '@ohif/core';
const { formatPN, formatDate } = utils;
-function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
+function usePatientInfo() {
+ const { servicesManager } = useSystem();
const { displaySetService } = servicesManager.services;
const [patientInfo, setPatientInfo] = useState({
diff --git a/extensions/default/src/init.ts b/extensions/default/src/init.ts
index 19bff11b2..6c449264d 100644
--- a/extensions/default/src/init.ts
+++ b/extensions/default/src/init.ts
@@ -11,16 +11,13 @@ const metadataProvider = classes.MetadataProvider;
* @param {Object} servicesManager
* @param {Object} configuration
*/
-export default function init({
- servicesManager,
- configuration = {},
- commandsManager,
-}: withAppTypes): void {
+export default function init({ servicesManager, commandsManager }: withAppTypes): void {
const { toolbarService, cineService, viewportGridService } = servicesManager.services;
toolbarService.registerEventForToolbarUpdate(cineService, [
cineService.EVENTS.CINE_STATE_CHANGED,
]);
+
// Add
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handleScalingModules);
@@ -52,10 +49,10 @@ export default function init({
toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => {
const { buttons } = state;
for (const [id, button] of Object.entries(buttons)) {
- const { groupId, items, listeners } = button.props || {};
+ const { buttonSection, items, listeners } = button.props || {};
// Handle group items' listeners
- if (groupId && items) {
+ if (buttonSection && items) {
items.forEach(item => {
if (item.listeners) {
subscribeToEvents(item.listeners);
diff --git a/extensions/default/src/utils/Toolbox.tsx b/extensions/default/src/utils/Toolbox.tsx
index 96b9ca48c..42781fd8e 100644
--- a/extensions/default/src/utils/Toolbox.tsx
+++ b/extensions/default/src/utils/Toolbox.tsx
@@ -27,7 +27,6 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
const [showConfig, setShowConfig] = useState(false);
const { toolbarButtons: toolboxSections, onInteraction } = useToolbar({
- servicesManager,
buttonSection: buttonSectionId,
});
diff --git a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx
index e0d62d782..49b46a460 100644
--- a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx
+++ b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx
@@ -1,14 +1,18 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
+import { useViewportRef } from '@ohif/core';
import './OHIFCornerstonePdfViewport.css';
-function OHIFCornerstonePdfViewport({ displaySets }) {
+function OHIFCornerstonePdfViewport({ displaySets, viewportId = 'pdf-viewport' }) {
const [url, setUrl] = useState(null);
+ const viewportElementRef = useRef(null);
+ const viewportRef = useViewportRef(viewportId);
useEffect(() => {
document.body.addEventListener('drag', makePdfDropTarget);
return function cleanup() {
document.body.removeEventListener('drag', makePdfDropTarget);
+ viewportRef.unregister();
};
}, []);
@@ -42,6 +46,11 @@ function OHIFCornerstonePdfViewport({ displaySets }) {
{
+ viewportElementRef.current = el;
+ if (el) viewportRef.register(el);
+ }}
+ data-viewport-id={viewportId}
>
{
+ if (trackedMeasurements?.context?.trackedSeries && trackedMeasurementsService) {
+ trackedMeasurementsService.updateTrackedSeries(trackedMeasurements.context.trackedSeries);
+ }
+ }, [trackedMeasurements?.context?.trackedSeries, trackedMeasurementsService]);
+
useEffect(() => {
// Update the state machine with the active viewport ID
sendTrackedMeasurementsEvent('UPDATE_ACTIVE_VIEWPORT_ID', {
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js
deleted file mode 100644
index 6503eae1a..000000000
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
-import { measurementTrackingMode } from './promptBeginTracking';
-
-const RESPONSE = {
- NO_NEVER: -1,
- CANCEL: 0,
- CREATE_REPORT: 1,
- ADD_SERIES: 2,
- SET_STUDY_AND_SERIES: 3,
- NO_NOT_FOR_SERIES: 4,
- HYDRATE_REPORT: 5,
-};
-
-function promptHydrateStructuredReport(
- { servicesManager, extensionManager, commandsManager, appConfig },
- ctx,
- evt
-) {
- const { uiViewportDialogService, displaySetService, customizationService } =
- servicesManager.services;
- const { viewportId, displaySetInstanceUID } = evt;
- const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
- return new Promise(async function (resolve, reject) {
- const standardMode = appConfig?.measurementTrackingMode === measurementTrackingMode.STANDARD;
-
- const promptResult = standardMode
- ? await _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId)
- : RESPONSE.HYDRATE_REPORT;
-
- // Need to do action here... So we can set state...
- let StudyInstanceUID, SeriesInstanceUIDs;
-
- if (promptResult === RESPONSE.HYDRATE_REPORT) {
- console.warn('!! HYDRATING STRUCTURED REPORT');
- const hydrationResult = hydrateStructuredReport(
- { servicesManager, extensionManager, commandsManager, appConfig },
- displaySetInstanceUID
- );
-
- StudyInstanceUID = hydrationResult.StudyInstanceUID;
- SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs;
- }
-
- resolve({
- userResponse: promptResult,
- displaySetInstanceUID: evt.displaySetInstanceUID,
- srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
- viewportId,
- StudyInstanceUID,
- SeriesInstanceUIDs,
- });
- });
-}
-
-function _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId) {
- return new Promise(function (resolve, reject) {
- const message = customizationService.getCustomization('viewportNotification.hydrateSRMessage');
- const actions = [
- {
- id: 'no-hydrate',
- type: 'secondary',
- text: 'No',
- value: RESPONSE.CANCEL,
- },
- {
- id: 'yes-hydrate',
- type: 'primary',
- text: 'Yes',
- value: RESPONSE.HYDRATE_REPORT,
- },
- ];
- const onSubmit = result => {
- uiViewportDialogService.hide();
- resolve(result);
- };
-
- uiViewportDialogService.show({
- viewportId,
- type: 'info',
- message,
- actions,
- onSubmit,
- onOutsideClick: () => {
- uiViewportDialogService.hide();
- resolve(RESPONSE.CANCEL);
- },
- onKeyPress: event => {
- if (event.key === 'Enter') {
- const action = actions.find(action => action.value === RESPONSE.HYDRATE_REPORT);
- onSubmit(action.value);
- }
- },
- });
- });
-}
-
-export default promptHydrateStructuredReport;
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.ts b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.ts
new file mode 100644
index 000000000..442fb2bae
--- /dev/null
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.ts
@@ -0,0 +1,30 @@
+import { utils } from '@ohif/extension-cornerstone';
+
+function promptHydrateStructuredReport({ servicesManager, commandsManager }, ctx, evt) {
+ const { displaySetService } = servicesManager.services;
+ const { viewportId, displaySetInstanceUID } = evt;
+ const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
+
+ const hydrateCallback = async () => {
+ return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
+ displaySet: srDisplaySet,
+ viewportId,
+ });
+ };
+
+ // For SR we need to use the whole context
+ const enhancedSrDisplaySet = {
+ ...srDisplaySet,
+ displaySetInstanceUID,
+ };
+
+ return utils.promptHydrationDialog({
+ servicesManager,
+ viewportId,
+ displaySet: enhancedSrDisplaySet,
+ hydrateCallback,
+ type: 'SR',
+ });
+}
+
+export default promptHydrateStructuredReport;
diff --git a/extensions/measurement-tracking/src/index.tsx b/extensions/measurement-tracking/src/index.tsx
index 0992e71a8..cc669e695 100644
--- a/extensions/measurement-tracking/src/index.tsx
+++ b/extensions/measurement-tracking/src/index.tsx
@@ -1,17 +1,16 @@
-import React from 'react';
-
import getContextModule from './getContextModule';
import getPanelModule from './getPanelModule';
import getViewportModule from './getViewportModule';
import { id } from './id.js';
-import { ViewportActionButton } from '@ohif/ui-next';
-import i18n from '@ohif/i18n';
import { measurementTrackingMode } from './contexts/TrackedMeasurementsContext/promptBeginTracking';
import getCustomizationModule from './getCustomizationModule';
import {
onDoubleClickHandler,
customOnDropHandlerCallback,
} from './customizations/studyBrowserCustomization';
+import { TrackedMeasurementsService } from './services';
+// Import types to ensure they're included in the build
+import './types';
const measurementTrackingExtension = {
/**
@@ -23,8 +22,25 @@ const measurementTrackingExtension = {
getPanelModule,
getViewportModule,
+ /**
+ * Service configuration
+ */
+ preRegistration({ servicesManager }) {
+ servicesManager.registerService(TrackedMeasurementsService.REGISTRATION);
+ },
+
onModeEnter({ servicesManager }) {
- const { toolbarService, customizationService } = servicesManager.services;
+ const { customizationService, toolbarService, trackedMeasurementsService } =
+ servicesManager.services;
+
+ toolbarService.registerEventForToolbarUpdate(trackedMeasurementsService, [
+ trackedMeasurementsService.EVENTS.TRACKED_SERIES_CHANGED,
+ trackedMeasurementsService.EVENTS.SERIES_ADDED,
+ trackedMeasurementsService.EVENTS.SERIES_REMOVED,
+ trackedMeasurementsService.EVENTS.TRACKING_ENABLED,
+ trackedMeasurementsService.EVENTS.TRACKING_DISABLED,
+ ]);
+
customizationService.setCustomizations({
'studyBrowser.thumbnailDoubleClickCallback': {
$set: onDoubleClickHandler,
@@ -33,23 +49,6 @@ const measurementTrackingExtension = {
$set: customOnDropHandlerCallback,
},
});
- toolbarService.addButtons(
- [
- {
- // A button for loading tracked, SR measurements.
- // Note that the command run is registered in TrackedMeasurementsContext
- // because it must be bound to a React context's data.
- id: 'loadSRMeasurements',
- component: props => (
- {i18n.t('Common:LOAD')}
- ),
- props: {
- commands: ['loadTrackedSRMeasurements'],
- },
- },
- ],
- true // replace the button if it is already defined
- );
},
getCustomizationModule,
};
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx
index 7b3b2094c..9612ba1c2 100644
--- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx
+++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx
@@ -1,12 +1,12 @@
import React from 'react';
-import { utils } from '@ohif/core';
+import { useSystem, utils } from '@ohif/core';
import { AccordionTrigger, MeasurementTable, ScrollArea, useViewportGrid } from '@ohif/ui-next';
import {
PanelMeasurement,
StudyMeasurements,
- StudyMeasurementsActions,
StudySummaryFromMetadata,
AccordionGroup,
+ StudyMeasurementsActions,
MeasurementsOrAdditionalFindings,
} from '@ohif/extension-cornerstone';
@@ -18,7 +18,9 @@ const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
function PanelMeasurementTableTracking(props) {
const [viewportGrid] = useViewportGrid();
- const { measurementService, uiModalService } = props.servicesManager.services;
+ const { servicesManager } = useSystem();
+ const { measurementService, uiModalService } = servicesManager.services;
+
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const measurementFilter = trackedStudy
diff --git a/extensions/measurement-tracking/src/services/TrackedMeasurementsService/TrackedMeasurementsService.ts b/extensions/measurement-tracking/src/services/TrackedMeasurementsService/TrackedMeasurementsService.ts
new file mode 100644
index 000000000..d657404af
--- /dev/null
+++ b/extensions/measurement-tracking/src/services/TrackedMeasurementsService/TrackedMeasurementsService.ts
@@ -0,0 +1,168 @@
+import { PubSubService } from '@ohif/core';
+
+const EVENTS = {
+ TRACKED_SERIES_CHANGED: 'event::trackedmeasurements:trackedserieschanged',
+ SERIES_ADDED: 'event::trackedmeasurements:seriesadded',
+ SERIES_REMOVED: 'event::trackedmeasurements:seriesremoved',
+ TRACKING_ENABLED: 'event::trackedmeasurements:trackingenabled',
+ TRACKING_DISABLED: 'event::trackedmeasurements:trackingdisabled',
+};
+
+/**
+ * Service class for accessing tracked measurements data.
+ * This service provides a robust way to access tracked series information
+ * from anywhere in the application, including outside of React components.
+ */
+export class TrackedMeasurementsService extends PubSubService {
+ public static readonly REGISTRATION = {
+ name: 'trackedMeasurementsService',
+ altName: 'TrackedMeasurementsService',
+ create: ({ configuration = {} }) => {
+ return new TrackedMeasurementsService();
+ },
+ };
+
+ private _trackedSeries: string[] = [];
+
+ constructor() {
+ super(EVENTS);
+ }
+
+ /**
+ * Updates the tracked series and notifies subscribers
+ * @param trackedSeries Array of series UIDs being tracked
+ */
+ public updateTrackedSeries(trackedSeries: string[]): void {
+ if (!trackedSeries) {
+ trackedSeries = [];
+ }
+
+ const hasChanged =
+ this._trackedSeries.length !== trackedSeries.length ||
+ this._trackedSeries.some((seriesUID, index) => seriesUID !== trackedSeries[index]);
+
+ if (hasChanged) {
+ const oldSeries = [...this._trackedSeries];
+ this._trackedSeries = [...trackedSeries];
+
+ const wasEmpty = oldSeries.length === 0;
+ const isEmpty = trackedSeries.length === 0;
+
+ if (wasEmpty && !isEmpty) {
+ this._broadcastEvent(EVENTS.TRACKING_ENABLED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ } else if (!wasEmpty && isEmpty) {
+ this._broadcastEvent(EVENTS.TRACKING_DISABLED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ }
+
+ this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ }
+ }
+
+ /**
+ * Adds a single series to tracking
+ * @param seriesInstanceUID Series instance UID to add to tracking
+ */
+ public addTrackedSeries(seriesInstanceUID: string): void {
+ if (!seriesInstanceUID || this.isSeriesTracked(seriesInstanceUID)) {
+ return;
+ }
+
+ const wasEmpty = this._trackedSeries.length === 0;
+ this._trackedSeries = [...this._trackedSeries, seriesInstanceUID];
+
+ this._broadcastEvent(EVENTS.SERIES_ADDED, {
+ seriesInstanceUID,
+ trackedSeries: this.getTrackedSeries(),
+ });
+
+ if (wasEmpty) {
+ this._broadcastEvent(EVENTS.TRACKING_ENABLED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ }
+
+ this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ }
+
+ /**
+ * Removes a single series from tracking
+ * @param seriesInstanceUID Series instance UID to remove from tracking
+ */
+ public removeTrackedSeries(seriesInstanceUID: string): void {
+ if (!seriesInstanceUID || !this.isSeriesTracked(seriesInstanceUID)) {
+ return;
+ }
+
+ this._trackedSeries = this._trackedSeries.filter(uid => uid !== seriesInstanceUID);
+
+ this._broadcastEvent(EVENTS.SERIES_REMOVED, {
+ seriesInstanceUID,
+ trackedSeries: this.getTrackedSeries(),
+ });
+
+ if (this._trackedSeries.length === 0) {
+ this._broadcastEvent(EVENTS.TRACKING_DISABLED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ }
+
+ this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
+ trackedSeries: this.getTrackedSeries(),
+ });
+ }
+
+ /**
+ * Retrieves the currently tracked series
+ * @returns Array of series UIDs being tracked
+ */
+ public getTrackedSeries(): string[] {
+ return [...this._trackedSeries];
+ }
+
+ /**
+ * Checks if a specific series is being tracked
+ * @param seriesInstanceUID Series instance UID to check
+ * @returns boolean indicating if series is tracked
+ */
+ public isSeriesTracked(seriesInstanceUID: string): boolean {
+ return this._trackedSeries.includes(seriesInstanceUID);
+ }
+
+ /**
+ * Resets the service state
+ */
+ public reset(): void {
+ const wasTracking = this._trackedSeries.length > 0;
+ this._trackedSeries = [];
+
+ if (wasTracking) {
+ this._broadcastEvent(EVENTS.TRACKING_DISABLED, {
+ trackedSeries: [],
+ });
+
+ this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
+ trackedSeries: [],
+ });
+ }
+
+ super.reset();
+ }
+
+ /**
+ * Checks if any series are being tracked
+ * @returns boolean indicating if tracking is active
+ */
+ public isTrackingEnabled(): boolean {
+ return this._trackedSeries.length > 0;
+ }
+}
+
+export default TrackedMeasurementsService;
diff --git a/extensions/measurement-tracking/src/services/TrackedMeasurementsService/index.ts b/extensions/measurement-tracking/src/services/TrackedMeasurementsService/index.ts
new file mode 100644
index 000000000..458114479
--- /dev/null
+++ b/extensions/measurement-tracking/src/services/TrackedMeasurementsService/index.ts
@@ -0,0 +1 @@
+export * from './TrackedMeasurementsService';
diff --git a/extensions/measurement-tracking/src/services/index.ts b/extensions/measurement-tracking/src/services/index.ts
new file mode 100644
index 000000000..458114479
--- /dev/null
+++ b/extensions/measurement-tracking/src/services/index.ts
@@ -0,0 +1 @@
+export * from './TrackedMeasurementsService';
diff --git a/extensions/measurement-tracking/src/types/AppTypes.ts b/extensions/measurement-tracking/src/types/AppTypes.ts
new file mode 100644
index 000000000..24552f61d
--- /dev/null
+++ b/extensions/measurement-tracking/src/types/AppTypes.ts
@@ -0,0 +1,11 @@
+/* eslint-disable @typescript-eslint/no-namespace */
+import { TrackedMeasurementsService } from '../services/TrackedMeasurementsService';
+
+declare global {
+ namespace AppTypes {
+ export type TrackedMeasurementsServiceType = TrackedMeasurementsService;
+ export interface Services {
+ trackedMeasurementsService?: TrackedMeasurementsService;
+ }
+ }
+}
diff --git a/extensions/measurement-tracking/src/types/index.ts b/extensions/measurement-tracking/src/types/index.ts
new file mode 100644
index 000000000..03f81d63a
--- /dev/null
+++ b/extensions/measurement-tracking/src/types/index.ts
@@ -0,0 +1 @@
+export * from './AppTypes';
diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx
index ed99a7238..74ebb313a 100644
--- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx
+++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx
@@ -2,32 +2,28 @@ import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { ViewportActionArrows } from '@ohif/ui-next';
-import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
+import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import { annotation } from '@cornerstonejs/tools';
import { useTrackedMeasurements } from './../getContextModule';
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
-import { useTranslation } from 'react-i18next';
+import { useSystem } from '@ohif/core';
function TrackedCornerstoneViewport(
props: withAppTypes<{ viewportId: string; displaySets: AppTypes.DisplaySet[] }>
) {
- const { displaySets, viewportId, servicesManager, extensionManager } = props;
+ const { servicesManager } = useSystem();
+ const { displaySets, viewportId } = props as {
+ displaySets: AppTypes.DisplaySet[];
+ viewportId: string;
+ servicesManager: AppTypes.Services;
+ };
- const {
- measurementService,
- cornerstoneViewportService,
- viewportGridService,
- viewportActionCornersService,
- } = servicesManager.services;
+ const { measurementService, cornerstoneViewportService, viewportGridService, toolbarService } =
+ servicesManager.services;
// Todo: handling more than one displaySet on the same viewport
const displaySet = displaySets[0];
- const { t } = useTranslation('Common');
-
- const [viewportGrid] = useViewportGrid();
- const { activeViewportId } = viewportGrid;
-
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const [isTracked, setIsTracked] = useState(false);
@@ -193,39 +189,9 @@ function TrackedCornerstoneViewport(
[measurementService, servicesManager, trackedMeasurementUID, trackedMeasurements, viewportId]
);
- useEffect(() => {
- const statusComponent = _getStatusComponent(isTracked, t);
- const arrowsComponent = _getArrowsComponent(
- isTracked,
- switchMeasurement,
- viewportId === activeViewportId
- );
-
- viewportActionCornersService.addComponents([
- {
- viewportId,
- id: 'viewportStatusComponent',
- component: statusComponent,
- indexPriority: -100,
- location: viewportActionCornersService.LOCATIONS.topRight,
- },
- {
- viewportId,
- id: 'viewportActionArrowsComponent',
- component: arrowsComponent,
- indexPriority: 0,
- location: viewportActionCornersService.LOCATIONS.topRight,
- },
- ]);
- }, [activeViewportId, isTracked, switchMeasurement, viewportActionCornersService, viewportId]);
-
const getCornerstoneViewport = () => {
- const { component: Component } = extensionManager.getModuleEntry(
- '@ohif/extension-cornerstone.viewportModule.cornerstone'
- );
-
return (
- {
props.onElementEnabled?.(evt);
@@ -316,30 +282,4 @@ const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) =>
);
};
-function _getStatusComponent(isTracked, t) {
- if (!isTracked) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
- {isTracked ? (
- <>{t('Series is tracked and can be viewed in the measurement panel')}>
- ) : (
- <>{t('Measurements for untracked series will not be shown in the measurements panel')}>
- )}
-
-
- );
-}
-
export default TrackedCornerstoneViewport;
diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx
index 89faeedae..f74941f3e 100644
--- a/extensions/tmtv/src/Panels/PanelPetSUV.tsx
+++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx
@@ -58,11 +58,10 @@ InputRow.Input = ({ className, ...props }) => (
InputRow.Label.displayName = 'InputRow.Label';
InputRow.Input.displayName = 'InputRow.Input';
-export default function PanelPetSUV({ servicesManager }: withAppTypes) {
- const { commandsManager } = useSystem();
+export default function PanelPetSUV() {
+ const { commandsManager, servicesManager } = useSystem();
const { t } = useTranslation('PanelSUV');
- const { displaySetService, toolGroupService, toolbarService, hangingProtocolService } =
- servicesManager.services;
+ const { displaySetService, hangingProtocolService } = servicesManager.services;
const [metadata, setMetadata] = useState(DEFAULT_MEATADATA);
const [ptDisplaySet, setPtDisplaySet] = useState(null);
diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
index 1a3b95098..cf5e0fd6b 100644
--- a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
+++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
@@ -9,7 +9,7 @@ export default function PanelRoiThresholdSegmentation() {
const { commandsManager, servicesManager } = useSystem();
const { segmentationService } = servicesManager.services;
const { segmentationsWithRepresentations: segmentationsInfo } =
- useActiveViewportSegmentationRepresentations({ servicesManager });
+ useActiveViewportSegmentationRepresentations();
const segmentationIds = segmentationsInfo?.map(info => info.segmentation.segmentationId) || [];
const segmentations = segmentationsInfo?.map(info => info.segmentation) || [];
diff --git a/modes/basic-dev-mode/src/index.ts b/modes/basic-dev-mode/src/index.ts
index d0036a544..c3f4ed8a8 100644
--- a/modes/basic-dev-mode/src/index.ts
+++ b/modes/basic-dev-mode/src/index.ts
@@ -94,8 +94,8 @@ function modeFactory({ modeConfiguration }) {
toolGroupService.createToolGroupAndAddTools('default', tools);
- toolbarService.addButtons(toolbarButtons);
- toolbarService.createButtonSection('primary', [
+ toolbarService.register(toolbarButtons);
+ toolbarService.updateSection('primary', [
'measurementSection',
'Zoom',
'WindowLevel',
diff --git a/modes/basic-dev-mode/src/toolbarButtons.ts b/modes/basic-dev-mode/src/toolbarButtons.ts
index 4b4d7ad88..c9f05ba8c 100644
--- a/modes/basic-dev-mode/src/toolbarButtons.ts
+++ b/modes/basic-dev-mode/src/toolbarButtons.ts
@@ -15,7 +15,6 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'measurementSection',
- groupId: 'measurementSection',
},
},
{
@@ -23,7 +22,6 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'moreToolsSection',
- groupId: 'MoreTools',
},
},
diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts
index 5390da1e7..2fe74aade 100644
--- a/modes/basic-test-mode/src/index.ts
+++ b/modes/basic-test-mode/src/index.ts
@@ -89,9 +89,9 @@ function modeFactory() {
'@ohif/extension-test.customizationModule.custom-context-menu',
]);
- toolbarService.addButtons(toolbarButtons);
+ toolbarService.register(toolbarButtons);
console.debug('toolbarButtons', toolbarButtons);
- toolbarService.createButtonSection('primary', [
+ toolbarService.updateSection(toolbarService.sections.primary, [
'MeasurementTools',
'Zoom',
'WindowLevelGroup',
@@ -103,7 +103,7 @@ function modeFactory() {
'MoreTools',
]);
- toolbarService.createButtonSection('windowLevelSection', [
+ toolbarService.updateSection('windowLevelSection', [
'WindowLevel',
'Soft tissue',
'Lung',
@@ -112,7 +112,42 @@ function modeFactory() {
'Brain',
]);
- toolbarService.createButtonSection('measurementSection', [
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ 'orientationMenu',
+ 'dataOverlayMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
+ 'advancedRenderingControls',
+ ]);
+
+ toolbarService.updateSection('advancedRenderingControls', [
+ 'voiManualControlMenu',
+ 'Colorbar',
+ 'opacityMenu',
+ 'thresholdMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
+ 'modalityLoadBadge',
+ 'trackingStatus',
+ 'navigationComponent',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
+ 'windowLevelMenu',
+ ]);
+
+ toolbarService.updateSection('windowLevelSection', [
+ 'WindowLevel',
+ 'Soft tissue',
+ 'Lung',
+ 'Liver',
+ 'Bone',
+ 'Brain',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.measurementSection, [
'Length',
'Bidirectional',
'ArrowAnnotate',
@@ -123,7 +158,7 @@ function modeFactory() {
'LivewireContour',
]);
- toolbarService.createButtonSection('moreToolsSection', [
+ toolbarService.updateSection(toolbarService.sections.moreToolsSection, [
'Reset',
'rotate-right',
'flipHorizontal',
diff --git a/modes/basic-test-mode/src/toolbarButtons.ts b/modes/basic-test-mode/src/toolbarButtons.ts
index 846c8f5aa..5491a8684 100644
--- a/modes/basic-test-mode/src/toolbarButtons.ts
+++ b/modes/basic-test-mode/src/toolbarButtons.ts
@@ -40,11 +40,11 @@ export const setToolActiveToolbar = {
},
};
-const ReferenceLinesListeners = [
+const callbacks = (toolName: string) => [
{
commandName: 'setViewportForToolConfiguration',
commandOptions: {
- toolName: 'ReferenceLines',
+ toolName,
},
},
];
@@ -55,7 +55,6 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'measurementSection',
- groupId: 'MeasurementTools',
},
},
{
@@ -63,19 +62,143 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'moreToolsSection',
- groupId: 'MoreTools',
},
},
{
id: 'WindowLevelGroup',
uiType: 'ohif.toolButtonList',
props: {
- groupId: 'WindowLevelGroup',
buttonSection: 'windowLevelSection',
},
},
// tool defs
+ {
+ id: 'advancedRenderingControls',
+ uiType: 'ohif.advancedRenderingControls',
+ props: {
+ evaluate: {
+ name: 'evaluate.advancedRenderingControls',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'modalityLoadBadge',
+ uiType: 'ohif.modalityLoadBadge',
+ props: {
+ icon: 'Status',
+ label: 'Status',
+ tooltip: 'Status',
+ evaluate: {
+ name: 'evaluate.modalityLoadBadge',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'navigationComponent',
+ uiType: 'ohif.navigationComponent',
+ props: {
+ icon: 'Navigation',
+ label: 'Navigation',
+ tooltip: 'Navigate between segments/measurements and manage their visibility',
+ evaluate: {
+ name: 'evaluate.navigationComponent',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'trackingStatus',
+ uiType: 'ohif.trackingStatus',
+ props: {
+ icon: 'TrackingStatus',
+ label: 'Tracking Status',
+ tooltip: 'View and manage tracking status of measurements and annotations',
+ evaluate: {
+ name: 'evaluate.trackingStatus',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'dataOverlayMenu',
+ uiType: 'ohif.dataOverlayMenu',
+ props: {
+ icon: 'ViewportViews',
+ label: 'Data Overlay',
+ tooltip: 'Configure data overlay options and manage foreground/background display sets',
+ evaluate: 'evaluate.dataOverlayMenu',
+ },
+ },
+ {
+ id: 'orientationMenu',
+ uiType: 'ohif.orientationMenu',
+ props: {
+ icon: 'OrientationSwitch',
+ label: 'Orientation',
+ tooltip:
+ 'Change viewport orientation between axial, sagittal, coronal and acquisition planes',
+ evaluate: {
+ name: 'evaluate.orientationMenu',
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenu',
+ uiType: 'ohif.windowLevelMenu',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: 'evaluate.windowLevelMenu',
+ },
+ },
+ {
+ id: 'voiManualControlMenu',
+ uiType: 'ohif.voiManualControlMenu',
+ props: {
+ icon: 'WindowLevelAdvanced',
+ label: 'Advanced Window Level',
+ tooltip: 'Advanced window/level settings with manual controls and presets',
+ evaluate: 'evaluate.voiManualControlMenu',
+ },
+ },
+ {
+ id: 'thresholdMenu',
+ uiType: 'ohif.thresholdMenu',
+ props: {
+ icon: 'Threshold',
+ label: 'Threshold',
+ tooltip: 'Image threshold settings',
+ evaluate: {
+ name: 'evaluate.thresholdMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'opacityMenu',
+ uiType: 'ohif.opacityMenu',
+ props: {
+ icon: 'Opacity',
+ label: 'Opacity',
+ tooltip: 'Image opacity settings',
+ evaluate: {
+ name: 'evaluate.opacityMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'Colorbar',
+ uiType: 'ohif.colorbar',
+ props: {
+ type: 'tool',
+ label: 'Colorbar',
+ },
+ },
_createWwwcPreset(1, 'Soft tissue', '400 / 40'),
_createWwwcPreset(2, 'Lung', '1500 / -600'),
_createWwwcPreset(3, 'Liver', '150 / 90'),
@@ -338,10 +461,6 @@ const toolbarButtons: Button[] = [
label: 'Reference Lines',
tooltip: 'Show Reference Lines',
commands: 'toggleEnabledDisabledToolbar',
- listeners: {
- [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
- [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
- },
evaluate: 'evaluate.cornerstoneTool.toggle',
},
},
diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts
index b24e1f250..2004eaf2b 100644
--- a/modes/longitudinal/src/index.ts
+++ b/modes/longitudinal/src/index.ts
@@ -90,8 +90,8 @@ function modeFactory({ modeConfiguration }) {
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
- toolbarService.addButtons(toolbarButtons);
- toolbarService.createButtonSection('primary', [
+ toolbarService.register(toolbarButtons);
+ toolbarService.updateSection(toolbarService.sections.primary, [
'MeasurementTools',
'Zoom',
'Pan',
@@ -103,7 +103,34 @@ function modeFactory({ modeConfiguration }) {
'MoreTools',
]);
- toolbarService.createButtonSection('measurementSection', [
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ 'orientationMenu',
+ 'dataOverlayMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
+ 'AdvancedRenderingControls',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.advancedRenderingControlsSection, [
+ 'windowLevelMenuEmbedded',
+ 'voiManualControlMenu',
+ 'Colorbar',
+ 'opacityMenu',
+ 'thresholdMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
+ 'modalityLoadBadge',
+ 'trackingStatus',
+ 'navigationComponent',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
+ 'windowLevelMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.measurementSection, [
'Length',
'Bidirectional',
'ArrowAnnotate',
@@ -115,7 +142,7 @@ function modeFactory({ modeConfiguration }) {
'LivewireContour',
]);
- toolbarService.createButtonSection('moreToolsSection', [
+ toolbarService.updateSection(toolbarService.sections.moreToolsSection, [
'Reset',
'rotate-right',
'flipHorizontal',
diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts
index 14c1e9c10..0f80e0a0e 100644
--- a/modes/longitudinal/src/toolbarButtons.ts
+++ b/modes/longitudinal/src/toolbarButtons.ts
@@ -26,7 +26,6 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'measurementSection',
- groupId: 'MeasurementTools',
},
},
{
@@ -34,10 +33,148 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'moreToolsSection',
- groupId: 'MoreTools',
+ },
+ },
+ {
+ id: 'AdvancedRenderingControls',
+ uiType: 'ohif.advancedRenderingControls',
+ props: {
+ buttonSection: 'advancedRenderingControlsSection',
},
},
// tool defs
+ {
+ id: 'modalityLoadBadge',
+ uiType: 'ohif.modalityLoadBadge',
+ props: {
+ icon: 'Status',
+ label: 'Status',
+ tooltip: 'Status',
+ evaluate: {
+ name: 'evaluate.modalityLoadBadge',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'navigationComponent',
+ uiType: 'ohif.navigationComponent',
+ props: {
+ icon: 'Navigation',
+ label: 'Navigation',
+ tooltip: 'Navigate between segments/measurements and manage their visibility',
+ evaluate: {
+ name: 'evaluate.navigationComponent',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'trackingStatus',
+ uiType: 'ohif.trackingStatus',
+ props: {
+ icon: 'TrackingStatus',
+ label: 'Tracking Status',
+ tooltip: 'View and manage tracking status of measurements and annotations',
+ evaluate: {
+ name: 'evaluate.trackingStatus',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'dataOverlayMenu',
+ uiType: 'ohif.dataOverlayMenu',
+ props: {
+ icon: 'ViewportViews',
+ label: 'Data Overlay',
+ tooltip: 'Configure data overlay options and manage foreground/background display sets',
+ evaluate: 'evaluate.dataOverlayMenu',
+ },
+ },
+ {
+ id: 'orientationMenu',
+ uiType: 'ohif.orientationMenu',
+ props: {
+ icon: 'OrientationSwitch',
+ label: 'Orientation',
+ tooltip:
+ 'Change viewport orientation between axial, sagittal, coronal and acquisition planes',
+ evaluate: {
+ name: 'evaluate.orientationMenu',
+ // hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenuEmbedded',
+ uiType: 'ohif.windowLevelMenuEmbedded',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: {
+ name: 'evaluate.windowLevelMenuEmbedded',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenu',
+ uiType: 'ohif.windowLevelMenu',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: {
+ name: 'evaluate.windowLevelMenu',
+ },
+ },
+ },
+ {
+ id: 'voiManualControlMenu',
+ uiType: 'ohif.voiManualControlMenu',
+ props: {
+ icon: 'WindowLevelAdvanced',
+ label: 'Advanced Window Level',
+ tooltip: 'Advanced window/level settings with manual controls and presets',
+ evaluate: 'evaluate.voiManualControlMenu',
+ },
+ },
+ {
+ id: 'thresholdMenu',
+ uiType: 'ohif.thresholdMenu',
+ props: {
+ icon: 'Threshold',
+ label: 'Threshold',
+ tooltip: 'Image threshold settings',
+ evaluate: {
+ name: 'evaluate.thresholdMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'opacityMenu',
+ uiType: 'ohif.opacityMenu',
+ props: {
+ icon: 'Opacity',
+ label: 'Opacity',
+ tooltip: 'Image opacity settings',
+ evaluate: {
+ name: 'evaluate.opacityMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'Colorbar',
+ uiType: 'ohif.colorbar',
+ props: {
+ type: 'tool',
+ label: 'Colorbar',
+ },
+ },
{
id: 'Reset',
uiType: 'ohif.toolButton',
diff --git a/modes/microscopy/src/index.tsx b/modes/microscopy/src/index.tsx
index dd49efcdc..7deabc564 100644
--- a/modes/microscopy/src/index.tsx
+++ b/modes/microscopy/src/index.tsx
@@ -47,10 +47,10 @@ function modeFactory({ modeConfiguration }) {
onModeEnter: ({ servicesManager }: withAppTypes) => {
const { toolbarService } = servicesManager.services;
- toolbarService.addButtons(toolbarButtons);
- toolbarService.createButtonSection('primary', ['MeasurementTools', 'dragPan', 'TagBrowser']);
+ toolbarService.register(toolbarButtons);
+ toolbarService.updateSection('primary', ['MeasurementTools', 'dragPan', 'TagBrowser']);
- toolbarService.createButtonSection('measurementSection', [
+ toolbarService.updateSection('measurementSection', [
'line',
'point',
'polygon',
diff --git a/modes/microscopy/src/toolbarButtons.ts b/modes/microscopy/src/toolbarButtons.ts
index ff4990b8f..1b3a6ab4e 100644
--- a/modes/microscopy/src/toolbarButtons.ts
+++ b/modes/microscopy/src/toolbarButtons.ts
@@ -14,7 +14,6 @@ const toolbarButtons: Button[] = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'measurementSection',
- groupId: 'MeasurementTools',
},
},
{
diff --git a/modes/preclinical-4d/src/index.tsx b/modes/preclinical-4d/src/index.tsx
index 4649e8ded..0080b7e72 100644
--- a/modes/preclinical-4d/src/index.tsx
+++ b/modes/preclinical-4d/src/index.tsx
@@ -54,9 +54,41 @@ function modeFactory({ modeConfiguration }) {
measurementService.clearMeasurements();
initToolGroups({ toolNames, Enums, toolGroupService, commandsManager, servicesManager });
- toolbarService.addButtons(toolbarButtons);
+ toolbarService.register(toolbarButtons);
- toolbarService.createButtonSection('secondary', ['ProgressDropdown']);
+ toolbarService.updateSection('secondary', ['ProgressDropdown']);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ 'orientationMenu',
+ 'dataOverlayMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ 'orientationMenu',
+ 'dataOverlayMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
+ 'AdvancedRenderingControls',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.advancedRenderingControlsSection, [
+ 'windowLevelMenuEmbedded',
+ 'voiManualControlMenu',
+ 'Colorbar',
+ 'opacityMenu',
+ 'thresholdMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
+ 'modalityLoadBadge',
+ 'trackingStatus',
+ 'navigationComponent',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
+ 'windowLevelMenu',
+ ]);
// the primary button section is created in the workflow steps
// specific to the step
diff --git a/modes/preclinical-4d/src/toolbarButtons.tsx b/modes/preclinical-4d/src/toolbarButtons.tsx
index fa9eb12df..6721b9615 100644
--- a/modes/preclinical-4d/src/toolbarButtons.tsx
+++ b/modes/preclinical-4d/src/toolbarButtons.tsx
@@ -1,4 +1,5 @@
import { toolGroupIds } from './initToolGroups';
+import { ViewportGridService } from '@ohif/core';
const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar',
@@ -7,12 +8,20 @@ const setToolActiveToolbar = {
},
};
+const callbacks = (toolName: string) => [
+ {
+ commandName: 'setViewportForToolConfiguration',
+ commandOptions: {
+ toolName,
+ },
+ },
+];
+
const toolbarButtons = [
{
id: 'MeasurementTools',
uiType: 'ohif.toolButtonList',
props: {
- groupId: 'MeasurementTools',
buttonSection: 'measurementSection',
},
},
@@ -20,7 +29,6 @@ const toolbarButtons = [
id: 'BrushTools',
uiType: 'ohif.toolBoxButtonGroup',
props: {
- groupId: 'BrushTools',
buttonSection: 'brushToolsSection',
},
},
@@ -28,10 +36,68 @@ const toolbarButtons = [
id: 'SegmentationTools',
uiType: 'ohif.toolBoxButton',
props: {
- groupId: 'SegmentationTools',
buttonSection: 'segmentationToolboxToolsSection',
},
},
+ {
+ id: 'AdvancedRenderingControls',
+ uiType: 'ohif.advancedRenderingControls',
+ props: {
+ buttonSection: 'advancedRenderingControlsSection',
+ },
+ },
+ {
+ id: 'modalityLoadBadge',
+ uiType: 'ohif.modalityLoadBadge',
+ props: {
+ icon: 'Status',
+ label: 'Status',
+ tooltip: 'Status',
+ evaluate: {
+ name: 'evaluate.modalityLoadBadge',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'navigationComponent',
+ uiType: 'ohif.navigationComponent',
+ props: {
+ icon: 'Navigation',
+ label: 'Navigation',
+ tooltip: 'Navigate between segments/measurements and manage their visibility',
+ evaluate: {
+ name: 'evaluate.navigationComponent',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenuEmbedded',
+ uiType: 'ohif.windowLevelMenuEmbedded',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: {
+ name: 'evaluate.windowLevelMenuEmbedded',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'trackingStatus',
+ uiType: 'ohif.trackingStatus',
+ props: {
+ icon: 'TrackingStatus',
+ label: 'Tracking Status',
+ tooltip: 'View and manage tracking status of measurements and annotations',
+ evaluate: {
+ name: 'evaluate.trackingStatus',
+ hideWhenDisabled: true,
+ },
+ },
+ },
{
id: 'Length',
uiType: 'ohif.toolButton',
@@ -330,6 +396,84 @@ const toolbarButtons = [
],
},
},
+ {
+ id: 'dataOverlayMenu',
+ uiType: 'ohif.dataOverlayMenu',
+ props: {
+ icon: 'ViewportViews',
+ label: 'Data Overlay',
+ tooltip: 'Configure data overlay options and manage foreground/background display sets',
+ evaluate: 'evaluate.dataOverlayMenu',
+ },
+ },
+ {
+ id: 'orientationMenu',
+ uiType: 'ohif.orientationMenu',
+ props: {
+ icon: 'OrientationSwitch',
+ label: 'Orientation',
+ tooltip:
+ 'Change viewport orientation between axial, sagittal, coronal and acquisition planes',
+ evaluate: {
+ name: 'evaluate.orientationMenu',
+ // hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenu',
+ uiType: 'ohif.windowLevelMenu',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: 'evaluate.windowLevelMenu',
+ },
+ },
+ {
+ id: 'voiManualControlMenu',
+ uiType: 'ohif.voiManualControlMenu',
+ props: {
+ icon: 'WindowLevelAdvanced',
+ label: 'Advanced Window Level',
+ tooltip: 'Advanced window/level settings with manual controls and presets',
+ evaluate: 'evaluate.voiManualControlMenu',
+ },
+ },
+ {
+ id: 'thresholdMenu',
+ uiType: 'ohif.thresholdMenu',
+ props: {
+ icon: 'Threshold',
+ label: 'Threshold',
+ tooltip: 'Image threshold settings',
+ evaluate: {
+ name: 'evaluate.thresholdMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'opacityMenu',
+ uiType: 'ohif.opacityMenu',
+ props: {
+ icon: 'Opacity',
+ label: 'Opacity',
+ tooltip: 'Image opacity settings',
+ evaluate: {
+ name: 'evaluate.opacityMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'Colorbar',
+ uiType: 'ohif.colorbar',
+ props: {
+ type: 'tool',
+ label: 'Colorbar',
+ },
+ },
];
export default toolbarButtons;
diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx
index c0a4ada98..bc6d702c2 100644
--- a/modes/segmentation/src/index.tsx
+++ b/modes/segmentation/src/index.tsx
@@ -61,9 +61,9 @@ function modeFactory({ modeConfiguration }) {
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
- toolbarService.addButtons(toolbarButtons);
+ toolbarService.register(toolbarButtons);
- toolbarService.createButtonSection('primary', [
+ toolbarService.updateSection(toolbarService.sections.primary, [
'WindowLevel',
'Pan',
'Zoom',
@@ -74,7 +74,34 @@ function modeFactory({ modeConfiguration }) {
'MoreTools',
]);
- toolbarService.createButtonSection('moreToolsSection', [
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ 'orientationMenu',
+ 'dataOverlayMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
+ 'AdvancedRenderingControls',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.advancedRenderingControlsSection, [
+ 'windowLevelMenuEmbedded',
+ 'voiManualControlMenu',
+ 'Colorbar',
+ 'opacityMenu',
+ 'thresholdMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
+ 'modalityLoadBadge',
+ 'trackingStatus',
+ 'navigationComponent',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
+ 'windowLevelMenu',
+ ]);
+
+ toolbarService.updateSection('moreToolsSection', [
'Reset',
'rotate-right',
'flipHorizontal',
@@ -87,22 +114,22 @@ function modeFactory({ modeConfiguration }) {
'TagBrowser',
]);
- toolbarService.createButtonSection('segmentationToolbox', [
+ toolbarService.updateSection('segmentationToolbox', [
'SegmentationUtilities',
'SegmentationTools',
]);
- toolbarService.createButtonSection('segmentationToolboxUtilitySection', [
+ toolbarService.updateSection('segmentationToolboxUtilitySection', [
'LabelmapSlicePropagation',
'InterpolateLabelmap',
'SegmentBidirectional',
]);
- toolbarService.createButtonSection('segmentationToolboxToolsSection', [
+ toolbarService.updateSection('segmentationToolboxToolsSection', [
'BrushTools',
'MarkerLabelmap',
'RegionSegmentPlus',
'Shapes',
]);
- toolbarService.createButtonSection('brushToolsSection', ['Brush', 'Eraser', 'Threshold']);
+ toolbarService.updateSection('brushToolsSection', ['Brush', 'Eraser', 'Threshold']);
},
onModeExit: ({ servicesManager }: withAppTypes) => {
const {
@@ -165,7 +192,7 @@ function modeFactory({ modeConfiguration }) {
props: {
leftPanels: [ohif.leftPanel],
leftPanelResizable: true,
- rightPanels: [cornerstone.panelTool, cornerstone.measurements],
+ rightPanels: [cornerstone.panelTool],
rightPanelResizable: true,
// leftPanelClosed: true,
viewports: [
diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts
index 54238ae9f..af4b91ac8 100644
--- a/modes/segmentation/src/toolbarButtons.ts
+++ b/modes/segmentation/src/toolbarButtons.ts
@@ -18,20 +18,155 @@ const callbacks = (toolName: string) => [
];
const toolbarButtons: Button[] = [
+ {
+ id: 'AdvancedRenderingControls',
+ uiType: 'ohif.advancedRenderingControls',
+ props: {
+ buttonSection: 'advancedRenderingControlsSection',
+ },
+ },
+ {
+ id: 'modalityLoadBadge',
+ uiType: 'ohif.modalityLoadBadge',
+ props: {
+ icon: 'Status',
+ label: 'Status',
+ tooltip: 'Status',
+ evaluate: {
+ name: 'evaluate.modalityLoadBadge',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'navigationComponent',
+ uiType: 'ohif.navigationComponent',
+ props: {
+ icon: 'Navigation',
+ label: 'Navigation',
+ tooltip: 'Navigate between segments/measurements and manage their visibility',
+ evaluate: {
+ name: 'evaluate.navigationComponent',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'trackingStatus',
+ uiType: 'ohif.trackingStatus',
+ props: {
+ icon: 'TrackingStatus',
+ label: 'Tracking Status',
+ tooltip: 'View and manage tracking status of measurements and annotations',
+ evaluate: {
+ name: 'evaluate.trackingStatus',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'dataOverlayMenu',
+ uiType: 'ohif.dataOverlayMenu',
+ props: {
+ icon: 'ViewportViews',
+ label: 'Data Overlay',
+ tooltip: 'Configure data overlay options and manage foreground/background display sets',
+ evaluate: 'evaluate.dataOverlayMenu',
+ },
+ },
+ {
+ id: 'orientationMenu',
+ uiType: 'ohif.orientationMenu',
+ props: {
+ icon: 'OrientationSwitch',
+ label: 'Orientation',
+ tooltip:
+ 'Change viewport orientation between axial, sagittal, coronal and acquisition planes',
+ evaluate: {
+ name: 'evaluate.orientationMenu',
+ // hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenuEmbedded',
+ uiType: 'ohif.windowLevelMenuEmbedded',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: {
+ name: 'evaluate.windowLevelMenuEmbedded',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenu',
+ uiType: 'ohif.windowLevelMenu',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: 'evaluate.windowLevelMenu',
+ },
+ },
+ {
+ id: 'voiManualControlMenu',
+ uiType: 'ohif.voiManualControlMenu',
+ props: {
+ icon: 'WindowLevelAdvanced',
+ label: 'Advanced Window Level',
+ tooltip: 'Advanced window/level settings with manual controls and presets',
+ evaluate: 'evaluate.voiManualControlMenu',
+ },
+ },
+ {
+ id: 'thresholdMenu',
+ uiType: 'ohif.thresholdMenu',
+ props: {
+ icon: 'Threshold',
+ label: 'Threshold',
+ tooltip: 'Image threshold settings',
+ evaluate: {
+ name: 'evaluate.thresholdMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'opacityMenu',
+ uiType: 'ohif.opacityMenu',
+ props: {
+ icon: 'Opacity',
+ label: 'Opacity',
+ tooltip: 'Image opacity settings',
+ evaluate: {
+ name: 'evaluate.opacityMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'Colorbar',
+ uiType: 'ohif.colorbar',
+ props: {
+ type: 'tool',
+ label: 'Colorbar',
+ },
+ },
// sections
{
id: 'MoreTools',
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'moreToolsSection',
- groupId: 'MoreTools',
},
},
{
id: 'BrushTools',
uiType: 'ohif.toolBoxButtonGroup',
props: {
- groupId: 'BrushTools',
buttonSection: 'brushToolsSection',
},
},
@@ -40,7 +175,6 @@ const toolbarButtons: Button[] = [
id: 'SegmentationUtilities',
uiType: 'ohif.toolBoxButton',
props: {
- groupId: 'SegmentationUtilities',
buttonSection: 'segmentationToolboxUtilitySection',
},
},
@@ -48,7 +182,6 @@ const toolbarButtons: Button[] = [
id: 'SegmentationTools',
uiType: 'ohif.toolBoxButton',
props: {
- groupId: 'SegmentationTools',
buttonSection: 'segmentationToolboxToolsSection',
},
},
diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts
index 0671846c3..c0262876f 100644
--- a/modes/tmtv/src/index.ts
+++ b/modes/tmtv/src/index.ts
@@ -88,28 +88,56 @@ function modeFactory({ modeConfiguration }) {
);
unsubscriptions.push(unsubscribe);
- toolbarService.addButtons(toolbarButtons);
- toolbarService.createButtonSection('primary', [
+ toolbarService.register(toolbarButtons);
+ toolbarService.updateSection(toolbarService.sections.primary, [
'MeasurementTools',
'Zoom',
+ 'Pan',
'WindowLevel',
'Crosshairs',
- 'Pan',
]);
- toolbarService.createButtonSection('measurementSection', [
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ 'orientationMenu',
+ 'dataOverlayMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomMiddle, [
+ 'AdvancedRenderingControls',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.advancedRenderingControlsSection, [
+ 'windowLevelMenuEmbedded',
+ 'voiManualControlMenu',
+ 'Colorbar',
+ 'opacityMenu',
+ 'thresholdMenu',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
+ 'modalityLoadBadge',
+ 'trackingStatus',
+ 'navigationComponent',
+ ]);
+
+ toolbarService.updateSection(toolbarService.sections.viewportActionMenu.bottomLeft, [
+ 'windowLevelMenu',
+ ]);
+
+ toolbarService.updateSection('measurementSection', [
'Length',
'Bidirectional',
'ArrowAnnotate',
'EllipticalROI',
]);
- toolbarService.createButtonSection('ROIThresholdToolbox', ['SegmentationTools']);
- toolbarService.createButtonSection('segmentationToolboxToolsSection', [
+ toolbarService.updateSection('ROIThresholdToolbox', ['SegmentationTools']);
+ toolbarService.updateSection('segmentationToolboxToolsSection', [
'RectangleROIStartEndThreshold',
'BrushTools',
]);
- toolbarService.createButtonSection('brushToolsSection', ['Brush', 'Eraser', 'Threshold']);
+ toolbarService.updateSection('brushToolsSection', ['Brush', 'Eraser', 'Threshold']);
customizationService.setCustomizations({
'panelSegmentation.tableMode': {
diff --git a/modes/tmtv/src/toolbarButtons.ts b/modes/tmtv/src/toolbarButtons.ts
index b76d13bd4..39e0eabba 100644
--- a/modes/tmtv/src/toolbarButtons.ts
+++ b/modes/tmtv/src/toolbarButtons.ts
@@ -13,14 +13,12 @@ const toolbarButtons = [
uiType: 'ohif.toolButtonList',
props: {
buttonSection: 'measurementSection',
- groupId: 'MeasurementTools',
},
},
{
id: 'SegmentationTools',
uiType: 'ohif.toolBoxButton',
props: {
- groupId: 'SegmentationTools',
buttonSection: 'segmentationToolboxToolsSection',
},
},
@@ -29,7 +27,73 @@ const toolbarButtons = [
uiType: 'ohif.toolBoxButtonGroup',
props: {
buttonSection: 'brushToolsSection',
- groupId: 'BrushTools',
+ },
+ },
+ {
+ id: 'AdvancedRenderingControls',
+ uiType: 'ohif.advancedRenderingControls',
+ props: {
+ buttonSection: 'advancedRenderingControlsSection',
+ },
+ },
+ {
+ id: 'modalityLoadBadge',
+ uiType: 'ohif.modalityLoadBadge',
+ props: {
+ icon: 'Status',
+ label: 'Status',
+ tooltip: 'Status',
+ evaluate: {
+ name: 'evaluate.modalityLoadBadge',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'Colorbar',
+ uiType: 'ohif.colorbar',
+ props: {
+ type: 'tool',
+ label: 'Colorbar',
+ },
+ },
+ {
+ id: 'navigationComponent',
+ uiType: 'ohif.navigationComponent',
+ props: {
+ icon: 'Navigation',
+ label: 'Navigation',
+ tooltip: 'Navigate between segments/measurements and manage their visibility',
+ evaluate: {
+ name: 'evaluate.navigationComponent',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenuEmbedded',
+ uiType: 'ohif.windowLevelMenuEmbedded',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: {
+ name: 'evaluate.windowLevelMenuEmbedded',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'trackingStatus',
+ uiType: 'ohif.trackingStatus',
+ props: {
+ icon: 'TrackingStatus',
+ label: 'Tracking Status',
+ tooltip: 'View and manage tracking status of measurements and annotations',
+ evaluate: {
+ name: 'evaluate.trackingStatus',
+ hideWhenDisabled: true,
+ },
},
},
{
@@ -295,6 +359,76 @@ const toolbarButtons = [
],
},
},
+ {
+ id: 'dataOverlayMenu',
+ uiType: 'ohif.dataOverlayMenu',
+ props: {
+ icon: 'ViewportViews',
+ label: 'Data Overlay',
+ tooltip: 'Configure data overlay options and manage foreground/background display sets',
+ evaluate: 'evaluate.dataOverlayMenu',
+ },
+ },
+ {
+ id: 'orientationMenu',
+ uiType: 'ohif.orientationMenu',
+ props: {
+ icon: 'OrientationSwitch',
+ label: 'Orientation',
+ tooltip:
+ 'Change viewport orientation between axial, sagittal, coronal and acquisition planes',
+ evaluate: {
+ name: 'evaluate.orientationMenu',
+ // hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'windowLevelMenu',
+ uiType: 'ohif.windowLevelMenu',
+ props: {
+ icon: 'WindowLevel',
+ label: 'Window Level',
+ tooltip: 'Adjust window/level presets and customize image contrast settings',
+ evaluate: 'evaluate.windowLevelMenu',
+ },
+ },
+ {
+ id: 'voiManualControlMenu',
+ uiType: 'ohif.voiManualControlMenu',
+ props: {
+ icon: 'WindowLevelAdvanced',
+ label: 'Advanced Window Level',
+ tooltip: 'Advanced window/level settings with manual controls and presets',
+ evaluate: 'evaluate.voiManualControlMenu',
+ },
+ },
+ {
+ id: 'thresholdMenu',
+ uiType: 'ohif.thresholdMenu',
+ props: {
+ icon: 'Threshold',
+ label: 'Threshold',
+ tooltip: 'Image threshold settings',
+ evaluate: {
+ name: 'evaluate.thresholdMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
+ {
+ id: 'opacityMenu',
+ uiType: 'ohif.opacityMenu',
+ props: {
+ icon: 'Opacity',
+ label: 'Opacity',
+ tooltip: 'Image opacity settings',
+ evaluate: {
+ name: 'evaluate.opacityMenu',
+ hideWhenDisabled: true,
+ },
+ },
+ },
];
export default toolbarButtons;
diff --git a/package.json b/package.json
index 735bbbf5c..7269c9eb6 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
"optionalDependencies": {
"@percy/cypress": "^3.1.1",
"@playwright/test": "^1.48.0",
- "cypress": "^14.1.0",
+ "cypress": "14.3.1",
"cypress-file-upload": "^5.0.8"
},
"devDependencies": {
diff --git a/platform/app/package.json b/platform/app/package.json
index 06ec22058..b9ca5e12d 100644
--- a/platform/app/package.json
+++ b/platform/app/package.json
@@ -53,7 +53,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
- "@cornerstonejs/dicom-image-loader": "^3.12.2",
+ "@cornerstonejs/dicom-image-loader": "^3.15.1",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.11.0-beta.32",
"@ohif/extension-cornerstone": "3.11.0-beta.32",
diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx
index 4963b494b..5e1be45fa 100644
--- a/platform/app/src/App.tsx
+++ b/platform/app/src/App.tsx
@@ -13,6 +13,7 @@ import {
HotkeysManager,
ServiceProvidersManager,
SystemContextProvider,
+ ViewportRefsProvider,
} from '@ohif/core';
import {
ThemeWrapper as ThemeWrapperNext,
@@ -26,7 +27,6 @@ import {
ModalProvider,
ViewportDialogProvider,
UserAuthenticationProvider,
- ViewportActionCornersProvider,
} from '@ohif/ui-next';
// Viewer Project
// TODO: Should this influence study list?
@@ -105,7 +105,6 @@ function App({
userAuthenticationService,
uiNotificationService,
customizationService,
- viewportActionCornersService,
} = servicesManager.services;
const providers = [
@@ -114,6 +113,7 @@ function App({
[I18nextProvider, { i18n }],
[ThemeWrapperNext],
[SystemContextProvider, { commandsManager, extensionManager, hotkeysManager, servicesManager }],
+ [ViewportRefsProvider],
[ViewportGridProvider, { service: viewportGridService }],
[ViewportDialogProvider, { service: uiViewportDialogService }],
[CineProvider, { service: cineService }],
@@ -121,7 +121,6 @@ function App({
[TooltipProvider],
[DialogProvider, { service: uiDialogService, dialog: ManagedDialog }],
[ModalProvider, { service: uiModalService, modal: ModalNext }],
- [ViewportActionCornersProvider, { service: viewportActionCornersService }],
[ShepherdJourneyProvider],
];
diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx
index 7687d5c4b..67563ed5e 100644
--- a/platform/app/src/components/ViewportGrid.tsx
+++ b/platform/app/src/components/ViewportGrid.tsx
@@ -4,13 +4,11 @@ import { ViewportGrid, ViewportPane } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import EmptyViewport from './EmptyViewport';
import { useAppConfig } from '@state';
-import { useViewportActionCornersWithGrid } from '../hooks';
function ViewerViewportGrid(props: withAppTypes) {
const { servicesManager, viewportComponents = [], dataSource, commandsManager } = props;
const [viewportGrid, viewportGridService] = useViewportGrid();
const [appConfig] = useAppConfig();
- const { initializeViewportCorners } = useViewportActionCornersWithGrid();
const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid;
const { numCols, numRows } = layout;
@@ -419,12 +417,6 @@ function ViewerViewportGrid(props: withAppTypes) {
isHangingProtocolLayout={isHangingProtocolLayout}
onElementEnabled={evt => {
viewportGridService.setViewportIsReady(viewportId, true);
-
- // Initialize the viewport action corners for this viewport
- if (evt?.detail?.element) {
- const elementRef = { current: evt.detail.element };
- initializeViewportCorners(viewportId, elementRef, displaySets, commandsManager);
- }
}}
/>
@@ -433,7 +425,7 @@ function ViewerViewportGrid(props: withAppTypes) {
}
return viewportPanes;
- }, [viewports, activeViewportId, viewportComponents, dataSource, initializeViewportCorners]);
+ }, [viewports, activeViewportId, viewportComponents, dataSource]);
/**
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
diff --git a/platform/app/src/hooks/index.js b/platform/app/src/hooks/index.js
index 44163c188..29c94cee1 100644
--- a/platform/app/src/hooks/index.js
+++ b/platform/app/src/hooks/index.js
@@ -1,5 +1,4 @@
import useDebounce from './useDebounce';
import useSearchParams from './useSearchParams';
-import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
-export { useDebounce, useSearchParams, useViewportActionCornersWithGrid };
+export { useDebounce, useSearchParams };
diff --git a/platform/app/src/hooks/index.ts b/platform/app/src/hooks/index.ts
index 2eb9011e0..29c94cee1 100644
--- a/platform/app/src/hooks/index.ts
+++ b/platform/app/src/hooks/index.ts
@@ -1,5 +1,4 @@
-import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
import useDebounce from './useDebounce';
import useSearchParams from './useSearchParams';
-export { useViewportActionCornersWithGrid, useDebounce, useSearchParams };
+export { useDebounce, useSearchParams };
diff --git a/platform/app/src/hooks/useViewportActionCornersWithGrid.ts b/platform/app/src/hooks/useViewportActionCornersWithGrid.ts
deleted file mode 100644
index d0f75092c..000000000
--- a/platform/app/src/hooks/useViewportActionCornersWithGrid.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { useCallback, MutableRefObject, useRef } from 'react';
-import { useViewportActionCorners, ViewportActionCornersLocations } from '@ohif/ui-next';
-import { useSystem } from '@ohif/core';
-
-/**
- * Hook that manages viewport action corner components for all viewports in the grid
- * @returns A function that can be called to initialize action corners for a viewport
- */
-export default function useViewportActionCornersWithGrid() {
- const [, api] = useViewportActionCorners();
- const { servicesManager } = useSystem();
- const { customizationService } = servicesManager.services;
-
- // Keep a ref to track processed viewports to avoid duplicates
- const processedViewports = useRef>(new Set());
-
- // Map of customization keys to their corresponding enum values
- const locationMap = {
- 'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft,
- 'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight,
- 'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft,
- 'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight,
- };
-
- // Function to process customizations for a viewport
- const initializeViewportCorners = useCallback(
- (
- viewportId: string,
- elementRef: MutableRefObject,
- displaySets: any[],
- commandsManager: any
- ) => {
- if (!viewportId || !elementRef?.current) {
- return;
- }
-
- // Prevent duplicate processing
- if (processedViewports.current.has(viewportId)) {
- return;
- }
-
- // Mark this viewport as processed
- processedViewports.current.add(viewportId);
-
- // Clear any existing components for this viewport
- api.clear(viewportId);
-
- // Process each location
- Object.entries(locationMap).forEach(([locationKey, locationValue]) => {
- const items = customizationService.getCustomization(locationKey);
- if (!items || !items.length) {
- return;
- }
-
- items.forEach(item => {
- try {
- if (typeof item.component === 'function') {
- // Use the component renderer provided directly in the item
- const component = item.component({
- viewportId,
- element: elementRef.current,
- displaySets,
- location: locationValue,
- commandsManager,
- });
-
- if (component) {
- api.addComponent({
- viewportId,
- id: item.id,
- component,
- location: locationValue,
- indexPriority: item.indexPriority,
- });
- }
- } else if (item.component) {
- // Handle static components
- api.addComponent({
- viewportId,
- id: item.id,
- component: item.component,
- location: locationValue,
- indexPriority: item.indexPriority,
- });
- }
- } catch (error) {
- console.error(`Error adding component ${item.id} to viewport corner:`, error);
- }
- });
- });
- },
- [api, customizationService]
- );
-
- // Cleanup function for unmounting viewports
- const cleanupViewportCorners = useCallback(
- (viewportId: string) => {
- if (!viewportId) {
- return;
- }
-
- // Remove from processed set
- processedViewports.current.delete(viewportId);
-
- // Clear from the store
- api.clear(viewportId);
- },
- [api]
- );
-
- return { initializeViewportCorners, cleanupViewportCorners };
-}
diff --git a/platform/app/src/routes/Mode/Mode.tsx b/platform/app/src/routes/Mode/Mode.tsx
index 94f1a1e34..328887d5b 100644
--- a/platform/app/src/routes/Mode/Mode.tsx
+++ b/platform/app/src/routes/Mode/Mode.tsx
@@ -83,10 +83,10 @@ export default function ModeRoute({
extensionManager.setActiveDataSource(dataSourceName);
}
- const dataSource = extensionManager.getActiveDataSource()[0];
+ const dataSource = extensionManager.getActiveDataSourceOrNull();
// Only handling one route per mode for now
- const route = mode.routes[0];
+ const route = mode.routes?.[0] ?? null;
useEffect(() => {
const loadExtensions = async () => {
diff --git a/platform/cli/templates/mode/src/index.tsx b/platform/cli/templates/mode/src/index.tsx
index c3cd36805..bcb637b45 100644
--- a/platform/cli/templates/mode/src/index.tsx
+++ b/platform/cli/templates/mode/src/index.tsx
@@ -48,8 +48,8 @@ function modeFactory({ modeConfiguration }) {
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
- toolbarService.addButtons([...toolbarButtons, ...moreTools]);
- toolbarService.createButtonSection('primary', [
+ toolbarService.register([...toolbarButtons, ...moreTools]);
+ toolbarService.updateSection('primary', [
'measurementSection',
'Zoom',
'Pan',
@@ -61,7 +61,7 @@ function modeFactory({ modeConfiguration }) {
'moreToolsSection',
]);
- toolbarService.createButtonSection('measurementSection', [
+ toolbarService.updateSection('measurementSection', [
'Length',
'Bidirectional',
'ArrowAnnotate',
@@ -73,7 +73,7 @@ function modeFactory({ modeConfiguration }) {
'LivewireContour',
]);
- toolbarService.createButtonSection('moreToolsSection', [
+ toolbarService.updateSection('moreToolsSection', [
'Reset',
'rotate-right',
'flipHorizontal',
diff --git a/platform/core/package.json b/platform/core/package.json
index 3e62fe183..db02662b0 100644
--- a/platform/core/package.json
+++ b/platform/core/package.json
@@ -37,8 +37,8 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
- "@cornerstonejs/core": "^3.12.2",
- "@cornerstonejs/dicom-image-loader": "^3.12.2",
+ "@cornerstonejs/core": "^3.15.1",
+ "@cornerstonejs/dicom-image-loader": "^3.15.1",
"@ohif/ui": "3.11.0-beta.32",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"
diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts
index 20e67f90a..677f8fe68 100644
--- a/platform/core/src/extensions/ExtensionManager.ts
+++ b/platform/core/src/extensions/ExtensionManager.ts
@@ -388,6 +388,10 @@ export default class ExtensionManager extends PubSubService {
return this.dataSourceMap[this._activeDataSourceName];
};
+ getActiveDataSourceOrNull = () => {
+ return this.dataSourceMap[this._activeDataSourceName]?.[0] ?? null;
+ };
+
/**
* Gets the data source definition for the given data source name.
* If no data source name is provided, the active data source definition is
diff --git a/platform/core/src/hooks/index.ts b/platform/core/src/hooks/index.ts
new file mode 100644
index 000000000..eb47ffed4
--- /dev/null
+++ b/platform/core/src/hooks/index.ts
@@ -0,0 +1,5 @@
+export * from './useToolbar';
+export * from './types';
+export * from './useViewportRef';
+export * from './useViewportSize';
+export * from './useViewportMousePosition';
diff --git a/platform/core/src/hooks/types.ts b/platform/core/src/hooks/types.ts
new file mode 100644
index 000000000..6c93fc58e
--- /dev/null
+++ b/platform/core/src/hooks/types.ts
@@ -0,0 +1,27 @@
+export interface ToolbarButtonActions {
+ // Lock/Unlock actions
+ lockItem: (itemId: string, viewportId?: string) => void;
+ unlockItem: (itemId: string, viewportId?: string) => void;
+ toggleLock: (itemId: string, viewportId?: string) => void;
+ isItemLocked: (itemId: string, viewportId?: string) => boolean;
+
+ // Visibility actions
+ showItem: (itemId: string, viewportId?: string) => void;
+ hideItem: (itemId: string, viewportId?: string) => void;
+ toggleVisibility: (itemId: string, viewportId?: string) => void;
+ isItemVisible: (itemId: string, viewportId?: string) => boolean;
+
+ // Open/Close actions (for menus)
+ openItem: (itemId: string, viewportId?: string) => void;
+ closeItem: (itemId: string, viewportId?: string) => void;
+ closeAllItems: (viewportId?: string) => void;
+ isItemOpen: (itemId: string, viewportId?: string) => boolean;
+
+ // Evaluation
+ evaluateButtonForViewport: (itemId: string, viewportId?: string) => any;
+}
+
+export interface ToolbarHookReturn extends ToolbarButtonActions {
+ toolbarButtons: any[]; // The display representation of toolbar buttons
+ onInteraction: (args: any) => void;
+}
diff --git a/platform/core/src/hooks/useActiveViewportDisplaySets.ts b/platform/core/src/hooks/useActiveViewportDisplaySets.ts
index 895e0aedc..1314fff21 100644
--- a/platform/core/src/hooks/useActiveViewportDisplaySets.ts
+++ b/platform/core/src/hooks/useActiveViewportDisplaySets.ts
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react';
import { DisplaySet } from '../types';
-
+import { useSystem } from '../';
/**
* Hook that listens for changes in the active viewport and its display sets.
* It returns the display sets associated with the active viewport.
@@ -8,7 +8,8 @@ import { DisplaySet } from '../types';
* @param servicesManager - Services manager instance
* @returns Array of display sets for the active viewport
*/
-const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
+const useActiveViewportDisplaySets = (): DisplaySet[] => {
+ const { servicesManager } = useSystem();
const { displaySetService, viewportGridService } = servicesManager.services;
// Move this function outside useEffect and memoize it
const getDisplaySetsForViewport = useCallback(
diff --git a/platform/core/src/hooks/useToolbar.tsx b/platform/core/src/hooks/useToolbar.tsx
index 4ca4e8fa4..33c2fe011 100644
--- a/platform/core/src/hooks/useToolbar.tsx
+++ b/platform/core/src/hooks/useToolbar.tsx
@@ -1,20 +1,26 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSystem } from '../contextProviders/SystemProvider';
+import { ToolbarHookReturn } from './types';
-export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
+export function useToolbar({ buttonSection = 'primary' }: withAppTypes): ToolbarHookReturn {
const { commandsManager, servicesManager } = useSystem();
const { toolbarService, viewportGridService } = servicesManager.services;
const { EVENTS } = toolbarService;
+ // Store all buttons returned by the toolbar service
const [toolbarButtons, setToolbarButtons] = useState(
- toolbarService.getButtonSection(buttonSection as string)
+ toolbarService.getButtonSection(buttonSection as string).filter(Boolean)
);
+ // Store state of open/closed menu items
+ // Note: We keep this in local state to avoid re-evaluating the toolbar on every interaction
+ const [openItemIds, setOpenItemIds] = useState>({});
+
// Callback function for handling toolbar interactions
const onInteraction = useCallback(
args => {
args.event?.stopPropagation?.();
- const viewportId = viewportGridService.getActiveViewportId();
+ const viewportId = args.viewportId || viewportGridService.getActiveViewportId();
const refreshProps = { viewportId };
const buttonProps = toolbarService.getButtonProps(args.itemId);
@@ -58,15 +64,17 @@ export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
buttonProps.commands = allCommands;
}
+
toolbarService.recordInteraction({ ...args, ...buttonProps }, { refreshProps });
},
- [toolbarService, viewportGridService, toolbarButtons]
+ [toolbarService, viewportGridService]
);
// Effect to handle toolbar modification events
useEffect(() => {
const handleToolbarModified = () => {
- setToolbarButtons(toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean));
+ const buttons = toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean);
+ setToolbarButtons(buttons);
};
const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => {
@@ -96,5 +104,163 @@ export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
return () => subscriptions.forEach(sub => sub.unsubscribe());
}, [viewportGridService, toolbarService]);
- return { toolbarButtons, onInteraction };
+ // Action API for toolbar buttons
+ const actions = useMemo(() => {
+ return {
+ // Lock/Unlock actions
+ lockItem: (itemId: string, viewportId?: string) => {
+ const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
+ const button = toolbarService.getButton(itemId);
+
+ if (button) {
+ // Set isLocked flag in button metadata
+ button.props = {
+ ...button.props,
+ isLocked: true,
+ };
+
+ // Re-evaluate the button
+ toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
+ }
+ },
+
+ unlockItem: (itemId: string, viewportId?: string) => {
+ const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
+ const button = toolbarService.getButton(itemId);
+
+ if (button) {
+ // Set isLocked flag in button metadata
+ button.props = {
+ ...button.props,
+ isLocked: false,
+ };
+
+ // Re-evaluate the button
+ toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
+ }
+ },
+
+ toggleLock: (itemId: string, viewportId?: string) => {
+ const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
+ const button = toolbarService.getButton(itemId);
+
+ if (button) {
+ // Toggle isLocked flag in button metadata
+ button.props = {
+ ...button.props,
+ isLocked: !button.props.isLocked,
+ };
+
+ // Re-evaluate the button
+ toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
+ }
+ },
+
+ isItemLocked: (itemId: string, viewportId?: string): boolean => {
+ const button = toolbarService.getButton(itemId);
+ return button?.props?.isLocked === true;
+ },
+
+ // Visibility actions - controlled by evaluator functions in toolbar items
+ showItem: (itemId: string, viewportId?: string) => {
+ const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
+ const button = toolbarService.getButton(itemId);
+
+ if (button) {
+ // Set isVisible flag in button metadata
+ button.props = {
+ ...button.props,
+ isVisible: true,
+ };
+
+ // Re-evaluate the button
+ toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
+ }
+ },
+
+ hideItem: (itemId: string, viewportId?: string) => {
+ const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
+ const button = toolbarService.getButton(itemId);
+
+ if (button) {
+ // Set isVisible flag in button metadata
+ button.props = {
+ ...button.props,
+ isVisible: false,
+ };
+
+ // Re-evaluate the button
+ toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
+ }
+ },
+
+ toggleVisibility: (itemId: string, viewportId?: string) => {
+ const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
+ const button = toolbarService.getButton(itemId);
+
+ if (button) {
+ // Toggle isVisible flag in button metadata
+ button.props = {
+ ...button.props,
+ isVisible: button.props.isVisible === false ? true : false,
+ };
+
+ // Re-evaluate the button
+ toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
+ }
+ },
+
+ isItemVisible: (itemId: string, viewportId?: string): boolean => {
+ const button = toolbarService.getButton(itemId);
+ // If isVisible is explicitly false, return false; otherwise return true
+ return button?.props?.isVisible !== false;
+ },
+
+ // Open/Close actions - managed in local state for performance
+ openItem: (itemId: string, viewportId?: string) => {
+ setOpenItemIds(prev => {
+ // Close all other items
+ const updated = {};
+ // Then set the current item to open
+ updated[itemId] = true;
+ return updated;
+ });
+ },
+
+ closeItem: (itemId: string, viewportId?: string) => {
+ setOpenItemIds(prev => ({
+ ...prev,
+ [itemId]: false,
+ }));
+ },
+
+ closeAllItems: (viewportId?: string) => {
+ setOpenItemIds({});
+ },
+
+ isItemOpen: (itemId: string, viewportId?: string): boolean => {
+ return openItemIds[itemId] === true;
+ },
+ };
+ }, [viewportGridService, toolbarService, openItemIds]);
+
+ if (!toolbarButtons?.length) {
+ return {
+ toolbarButtons: [],
+ onInteraction,
+ ...actions,
+ };
+ }
+
+ // filter out buttons that are disabled and have hideWhenDisabled set to true
+ const filteredToolbarButtons = toolbarButtons.filter(button => {
+ const props = button.componentProps;
+ return props.visible !== false;
+ });
+
+ return {
+ toolbarButtons: filteredToolbarButtons,
+ onInteraction,
+ ...actions,
+ };
}
diff --git a/platform/core/src/hooks/useViewportMousePosition.ts b/platform/core/src/hooks/useViewportMousePosition.ts
new file mode 100644
index 000000000..60fa12c80
--- /dev/null
+++ b/platform/core/src/hooks/useViewportMousePosition.ts
@@ -0,0 +1,67 @@
+import { useState, useEffect } from 'react';
+import { useViewportRef } from './useViewportRef';
+import { useViewportSize } from './useViewportSize';
+
+interface MousePosition {
+ x: number;
+ y: number;
+ isInViewport: boolean;
+ relativeY: number; // Position as percentage from top (0-1)
+ isInBottomPercentage: (percentage: number) => boolean;
+}
+
+function useViewportMousePosition(viewportId: string): MousePosition {
+ const viewportRef = useViewportRef(viewportId);
+ const { height, clientRect } = useViewportSize(viewportId);
+
+ const [mousePosition, setMousePosition] = useState({
+ x: 0,
+ y: 0,
+ isInViewport: false,
+ relativeY: 0,
+ isInBottomPercentage: (percentage: number) => false,
+ });
+
+ useEffect(() => {
+ if (!viewportRef.current) {
+ return;
+ }
+
+ const handleMouseMove = (event: MouseEvent) => {
+ if (!clientRect) {
+ return;
+ }
+
+ // Get mouse position relative to viewport
+ const x = event.clientX - clientRect.left;
+ const y = event.clientY - clientRect.top;
+
+ const isInViewport = x >= 0 && x <= clientRect.width && y >= 0 && y <= clientRect.height;
+
+ const relativeY = Math.max(0, Math.min(1, y / height));
+
+ const isInBottomPercentage = (percentage: number) => {
+ return relativeY >= 1 - percentage / 100;
+ };
+
+ setMousePosition({
+ x,
+ y,
+ isInViewport,
+ relativeY,
+ isInBottomPercentage,
+ });
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ };
+ }, [viewportRef, height, clientRect]);
+
+ return mousePosition;
+}
+
+export default useViewportMousePosition;
+export { useViewportMousePosition };
diff --git a/platform/core/src/hooks/useViewportRef.ts b/platform/core/src/hooks/useViewportRef.ts
new file mode 100644
index 000000000..d6fd4eaba
--- /dev/null
+++ b/platform/core/src/hooks/useViewportRef.ts
@@ -0,0 +1,58 @@
+import React, { createContext, useContext, useRef } from 'react';
+
+type ViewportRefsContextType = {
+ registerViewport: (viewportId: string, element: HTMLElement) => void;
+ unregisterViewport: (viewportId: string) => void;
+ getViewportElement: (viewportId: string) => HTMLElement | null;
+ viewportRefs: Map;
+};
+
+// comment
+const ViewportRefsContext = createContext(undefined);
+
+export const ViewportRefsProvider = ({ children }: { children: React.ReactNode }) => {
+ const viewportRefsRef = useRef>(new Map());
+
+ const registerViewport = (viewportId: string, element: HTMLElement) => {
+ viewportRefsRef.current.set(viewportId, element);
+ };
+
+ const unregisterViewport = (viewportId: string) => {
+ viewportRefsRef.current.delete(viewportId);
+ };
+
+ const getViewportElement = (viewportId: string): HTMLElement | null => {
+ return viewportRefsRef.current.get(viewportId) || null;
+ };
+
+ const contextValue: ViewportRefsContextType = {
+ registerViewport,
+ unregisterViewport,
+ getViewportElement,
+ viewportRefs: viewportRefsRef.current,
+ };
+
+ return React.createElement(ViewportRefsContext.Provider, { value: contextValue }, children);
+};
+
+export const useViewportRefs = () => {
+ const context = useContext(ViewportRefsContext);
+
+ if (context === undefined) {
+ throw new Error('useViewportRefs must be used within a ViewportRefsProvider');
+ }
+
+ return context;
+};
+
+export const useViewportRef = (viewportId: string) => {
+ const { registerViewport, unregisterViewport, getViewportElement } = useViewportRefs();
+
+ const ref = {
+ current: getViewportElement(viewportId),
+ register: (element: HTMLElement) => registerViewport(viewportId, element),
+ unregister: () => unregisterViewport(viewportId),
+ };
+
+ return ref;
+};
diff --git a/platform/core/src/hooks/useViewportSize.ts b/platform/core/src/hooks/useViewportSize.ts
new file mode 100644
index 000000000..6ffe60bd7
--- /dev/null
+++ b/platform/core/src/hooks/useViewportSize.ts
@@ -0,0 +1,97 @@
+import { useEffect, useState, useCallback, useMemo } from 'react';
+import { useViewportRef } from './';
+
+interface ViewportSize {
+ width: number;
+ height: number;
+ offsetLeft: number;
+ offsetTop: number;
+ clientRect: DOMRect | null;
+ isVisible: boolean;
+}
+
+/**
+ * Hook that provides viewport size dimensions and monitors for changes
+ * @param viewportId - The ID of the viewport to monitor
+ * @returns ViewportSize object containing width, height, and visibility info
+ */
+function useViewportSize(viewportId: string): ViewportSize {
+ const viewportElementRef = useViewportRef(viewportId);
+
+ const [size, setSize] = useState({
+ width: 0,
+ height: 0,
+ offsetLeft: 0,
+ offsetTop: 0,
+ clientRect: null,
+ isVisible: false,
+ });
+
+ // Update viewport dimensions
+ const updateViewportSize = useCallback(() => {
+ if (!viewportElementRef?.current) {
+ return;
+ }
+
+ const element = viewportElementRef.current;
+ const clientRect = element.getBoundingClientRect();
+ const newWidth = clientRect.width;
+ const newHeight = clientRect.height;
+ const newOffsetLeft = element.offsetLeft;
+ const newOffsetTop = element.offsetTop;
+ const newIsVisible = newWidth > 0 && newHeight > 0;
+
+ setSize(prevSize => {
+ if (
+ prevSize.width === newWidth &&
+ prevSize.height === newHeight &&
+ prevSize.offsetLeft === newOffsetLeft &&
+ prevSize.offsetTop === newOffsetTop &&
+ prevSize.isVisible === newIsVisible
+ ) {
+ return prevSize;
+ }
+
+ return {
+ width: newWidth,
+ height: newHeight,
+ offsetLeft: newOffsetLeft,
+ offsetTop: newOffsetTop,
+ clientRect,
+ isVisible: newIsVisible,
+ };
+ });
+ }, [viewportElementRef]);
+
+ useEffect(() => {
+ if (!viewportId || !viewportElementRef?.current) {
+ return;
+ }
+
+ updateViewportSize();
+
+ const resizeObserver = new ResizeObserver(() => {
+ updateViewportSize();
+ });
+
+ resizeObserver.observe(viewportElementRef.current);
+
+ window.addEventListener('resize', updateViewportSize);
+
+ return () => {
+ window.removeEventListener('resize', updateViewportSize);
+
+ if (viewportElementRef.current) {
+ resizeObserver.unobserve(viewportElementRef.current);
+ }
+ resizeObserver.disconnect();
+ };
+ }, [viewportId, viewportElementRef, updateViewportSize]);
+
+ const memoizedSize = useMemo(() => size, [size]);
+
+ return memoizedSize;
+}
+
+export default useViewportSize;
+export { useViewportSize };
diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts
index b9d8719f5..134f471d2 100644
--- a/platform/core/src/index.ts
+++ b/platform/core/src/index.ts
@@ -2,6 +2,7 @@ import { ExtensionManager, MODULE_TYPES } from './extensions';
import { ServiceProvidersManager, ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes';
import { SystemContextProvider, useSystem } from './contextProviders/SystemProvider';
+import { ViewportRefsProvider } from './hooks/useViewportRef';
import DICOMWeb from './DICOMWeb';
import errorHandler from './errorHandler.js';
@@ -13,7 +14,6 @@ import utils from './utils';
import defaults from './defaults';
import * as Types from './types';
import * as Enums from './enums';
-import { useToolbar } from './hooks/useToolbar';
import {
CineService,
UIDialogService,
@@ -42,6 +42,8 @@ import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetS
import IWebApiDataSource from './DataSources/IWebApiDataSource';
import useActiveViewportDisplaySets from './hooks/useActiveViewportDisplaySets';
+export * from './hooks';
+
const hotkeys = {
...utils.hotkeys,
defaults: { hotkeyBindings: defaults.hotkeyBindings },
@@ -86,7 +88,6 @@ const OHIF = {
pubSubServiceInterface,
PubSubService,
PanelService,
- useToolbar,
useActiveViewportDisplaySets,
WorkflowStepsService,
StudyPrefetcherService,
@@ -101,6 +102,7 @@ export {
ServicesManager,
ServiceProvidersManager,
SystemContextProvider,
+ ViewportRefsProvider,
//
defaults,
utils,
@@ -137,7 +139,6 @@ export {
WorkflowStepsService,
StudyPrefetcherService,
useSystem,
- useToolbar,
useActiveViewportDisplaySets,
};
diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts
index 7edf04bba..99252b29e 100644
--- a/platform/core/src/services/ToolBarService/ToolbarService.ts
+++ b/platform/core/src/services/ToolBarService/ToolbarService.ts
@@ -2,13 +2,68 @@ import { CommandsManager } from '../../classes';
import { ExtensionManager } from '../../extensions';
import { PubSubService } from '../_shared/pubSubServiceInterface';
import type { RunCommand } from '../../types/Command';
-import { Button, ButtonProps, EvaluateFunction, EvaluatePublic, NestedButtonProps } from './types';
+import { Button, ButtonProps, EvaluateFunction, EvaluatePublic } from './types';
const EVENTS = {
TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified',
TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified',
};
+/**
+ * Predefined toolbar sections used throughout the application
+ */
+export const TOOLBAR_SECTIONS = {
+ /**
+ * Main toolbar
+ */
+ primary: 'primary',
+
+ /**
+ * Secondary toolbar
+ */
+ secondary: 'secondary',
+
+ /**
+ * Viewport action menu sections
+ */
+ viewportActionMenu: {
+ topLeft: 'viewportActionMenu.topLeft',
+ topRight: 'viewportActionMenu.topRight',
+ bottomLeft: 'viewportActionMenu.bottomLeft',
+ bottomRight: 'viewportActionMenu.bottomRight',
+ topMiddle: 'viewportActionMenu.topMiddle',
+ bottomMiddle: 'viewportActionMenu.bottomMiddle',
+ leftMiddle: 'viewportActionMenu.leftMiddle',
+ rightMiddle: 'viewportActionMenu.rightMiddle',
+ },
+
+ /**
+ * Measurement tools section
+ */
+ measurementSection: 'measurementSection',
+
+ /**
+ * More tools section
+ */
+ moreToolsSection: 'moreToolsSection',
+
+ /**
+ * Advanced rendering controls section
+ */
+ advancedRenderingControlsSection: 'advancedRenderingControlsSection',
+};
+
+export enum ButtonLocation {
+ TopLeft = 0,
+ TopMiddle = 1,
+ TopRight = 2,
+ LeftMiddle = 3,
+ RightMiddle = 4,
+ BottomLeft = 5,
+ BottomMiddle = 6,
+ BottomRight = 7,
+}
+
export default class ToolbarService extends PubSubService {
public static REGISTRATION = {
name: 'toolbarService',
@@ -18,6 +73,13 @@ export default class ToolbarService extends PubSubService {
},
};
+ /**
+ * Access to predefined toolbar sections for autocomplete support
+ */
+ public get sections() {
+ return TOOLBAR_SECTIONS;
+ }
+
public static createButton(options: {
id: string;
label: string;
@@ -130,7 +192,7 @@ export default class ToolbarService extends PubSubService {
* @param buttons - The buttons to be added.
* @param replace - Flag indicating if any existing button with the same id as one being added should be replaced
*/
- public addButtons(buttons: Button[], replace: boolean = false): void {
+ public register(buttons: Button[], replace: boolean = false): void {
buttons.forEach(button => {
if (replace || !this.state.buttons[button.id]) {
if (!button.props) {
@@ -231,7 +293,8 @@ export default class ToolbarService extends PubSubService {
* which buttons to evaluate based on the props
*/
public refreshToolbarState(refreshProps) {
- const buttons = this.state.buttons;
+ const originalButtons = this.state.buttons;
+ const updatedButtons = { ...originalButtons };
const evaluationResults = new Map();
const evaluateButtonProps = (button, props, refreshProps) => {
@@ -239,6 +302,7 @@ export default class ToolbarService extends PubSubService {
const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id);
return { ...props, disabled, disabledText, className, isActive };
} else {
+ const evaluateProps = props.evaluateProps;
const evaluated =
typeof props.evaluate === 'function'
? props.evaluate({ ...refreshProps, button })
@@ -247,6 +311,7 @@ export default class ToolbarService extends PubSubService {
...props,
...evaluated,
disabled: evaluated?.disabled || false,
+ visible: evaluateProps?.hideWhenDisabled && evaluated?.disabled ? false : true,
className: evaluated?.className || '',
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
};
@@ -255,7 +320,14 @@ export default class ToolbarService extends PubSubService {
}
};
- Object.values(buttons).forEach(button => {
+ const updatedIds = new Set();
+ Object.values(originalButtons).forEach(button => {
+ // Note: do not re-evaluate buttons that have already been evaluated
+ // this will result in inconsistencies in the toolbar state
+ if (updatedIds.has(button.id)) {
+ return;
+ }
+
const hasSection = (button.props as NestedButtonProps)?.buttonSection;
if (!hasSection) {
@@ -263,10 +335,12 @@ export default class ToolbarService extends PubSubService {
const buttonProps = button.props as ButtonProps;
const updatedProps = evaluateButtonProps(button, buttonProps, refreshProps);
- buttons[button.id] = {
+ updatedButtons[button.id] = {
...button,
props: updatedProps,
};
+
+ updatedIds.add(button.id);
} else {
let buttonProps = button.props as NestedButtonProps;
const { evaluate: groupEvaluate } = buttonProps;
@@ -288,17 +362,27 @@ export default class ToolbarService extends PubSubService {
}
toolButtonIds.forEach(buttonId => {
- const button = buttons[buttonId];
+ const button = originalButtons[buttonId];
+ if (!button) {
+ return;
+ }
+
+ if (updatedIds.has(buttonId)) {
+ return;
+ }
+
const updatedProps = evaluateButtonProps(button, button.props, refreshProps);
- buttons[buttonId] = {
+ updatedButtons[buttonId] = {
...button,
props: updatedProps,
};
+
+ updatedIds.add(buttonId);
});
}
});
- this.setButtons(buttons);
+ this.setButtons(updatedButtons);
return this.state;
}
@@ -373,7 +457,7 @@ export default class ToolbarService extends PubSubService {
* @param {string} key - The key of the button section.
* @param {Array} buttons - The buttons to be added to the section.
*/
- createButtonSection(key, buttons) {
+ updateSection(key, buttons) {
if (this.state.buttonSections[key]) {
this.state.buttonSections[key].push(
...buttons.filter(
@@ -443,7 +527,7 @@ export default class ToolbarService extends PubSubService {
}
const { id, uiType } = btn;
- const { groupId } = btn.props as NestedButtonProps;
+ const { buttonSection } = btn.props;
const buttonTypes = this._getButtonUITypes();
@@ -457,7 +541,7 @@ export default class ToolbarService extends PubSubService {
return;
}
- !groupId ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
+ !buttonSection ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
const { id: buttonId, props: componentProps } = btn;
@@ -587,6 +671,7 @@ export default class ToolbarService extends PubSubService {
return evaluateFunction;
});
+ const evaluateProps = props.evaluate;
props.evaluate = args => {
const results = evaluators.map(evaluator => evaluator(args)).filter(Boolean);
@@ -607,6 +692,8 @@ export default class ToolbarService extends PubSubService {
return mergedResult;
};
+ props.evaluateProps = evaluateProps;
+
return;
}
@@ -627,7 +714,9 @@ export default class ToolbarService extends PubSubService {
const { name, ...options } = evaluate;
const evaluateFunction = this._evaluateFunction[name];
if (evaluateFunction) {
+ const evaluateProps = props.evaluate;
props.evaluate = args => evaluateFunction({ ...args, ...options });
+ props.evaluateProps = evaluateProps;
return;
}
@@ -645,4 +734,56 @@ export default class ToolbarService extends PubSubService {
this.state.buttonSections[buttonSection] = [];
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
}
+
+ /**
+ * Checks if a button exists in any toolbar section.
+ *
+ * @param buttonId - The button ID to check for
+ * @returns True if the button exists in any section, false otherwise
+ */
+ isInAnySection(buttonId: string): boolean {
+ if (!buttonId) {
+ return false;
+ }
+
+ // Check all sections to see if the button ID exists in any of them
+ return Object.values(this.state.buttonSections).some(
+ section => Array.isArray(section) && section.includes(buttonId)
+ );
+ }
+
+ /**
+ * Returns the alignment and side for a specific viewport corner location.
+ * Used for menu positioning based on the corner location.
+ *
+ * @param location - The viewport corner location
+ * @returns An object with align and side properties
+ */
+ public getAlignAndSide(location: ButtonLocation | string): {
+ align: 'start' | 'end' | 'center';
+ side: 'top' | 'bottom' | 'left' | 'right';
+ } {
+ const locationNumber = Number(location);
+ switch (locationNumber) {
+ case ButtonLocation.TopLeft: // Enum 0, Original 0 (topLeft)
+ return { align: 'start', side: 'bottom' };
+ case ButtonLocation.TopMiddle: // Enum 1, Original 4 (topMiddle)
+ return { align: 'center', side: 'bottom' };
+ case ButtonLocation.TopRight: // Enum 2, Original 1 (topRight)
+ return { align: 'end', side: 'bottom' };
+ case ButtonLocation.LeftMiddle: // Enum 3, Original 6 (leftMiddle)
+ return { align: 'start', side: 'right' };
+ case ButtonLocation.RightMiddle: // Enum 4, Original 7 (rightMiddle)
+ return { align: 'end', side: 'left' };
+ case ButtonLocation.BottomLeft: // Enum 5, Original 2 (bottomLeft)
+ return { align: 'start', side: 'top' };
+ case ButtonLocation.BottomMiddle: // Enum 6, Original 5 (bottomMiddle)
+ return { align: 'center', side: 'top' };
+ case ButtonLocation.BottomRight: // Enum 7, Original 3 (bottomRight)
+ return { align: 'end', side: 'top' };
+ default:
+ // Default to TopLeft behavior if an unexpected value is passed.
+ return { align: 'start', side: 'bottom' };
+ }
+ }
}
diff --git a/platform/core/src/services/ToolBarService/index.ts b/platform/core/src/services/ToolBarService/index.ts
index 0147896b4..0ff1702a7 100644
--- a/platform/core/src/services/ToolBarService/index.ts
+++ b/platform/core/src/services/ToolBarService/index.ts
@@ -1,3 +1,3 @@
-import ToolbarService from './ToolbarService';
-
+import ToolbarService, { TOOLBAR_SECTIONS } from './ToolbarService';
export default ToolbarService;
+export { TOOLBAR_SECTIONS };
diff --git a/platform/core/src/services/ToolBarService/types.ts b/platform/core/src/services/ToolBarService/types.ts
index ed424a2ea..02ed1afe6 100644
--- a/platform/core/src/services/ToolBarService/types.ts
+++ b/platform/core/src/services/ToolBarService/types.ts
@@ -1,6 +1,22 @@
import type { RunCommand } from '../../types/Command';
import React from 'react';
+/**
+ * Type definitions for toolbar sections
+ */
+export type ToolbarSections = {
+ primary: string;
+ secondary: string;
+ viewportActionMenu: {
+ topLeft: string;
+ topRight: string;
+ bottomLeft: string;
+ bottomRight: string;
+ };
+ measurementSection: string;
+ moreToolsSection: string;
+};
+
export type EvaluatePublic =
| string
| EvaluateFunction
diff --git a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts
index 33a9b5449..ee2a0c6ab 100644
--- a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts
+++ b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts
@@ -137,7 +137,7 @@ class WorkflowStepsService extends PubSubService {
toUse.forEach(({ buttonSection, buttons }) => {
toolbarService.clearButtonSection(buttonSection);
- toolbarService.createButtonSection(buttonSection, buttons);
+ toolbarService.updateSection(buttonSection, buttons);
});
}
diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts
index 2c410d8ab..c2061c41c 100644
--- a/platform/core/src/types/DisplaySet.ts
+++ b/platform/core/src/types/DisplaySet.ts
@@ -35,9 +35,19 @@ export type DisplaySet = {
*/
renderedUrl?: string;
+ /**
+ * The instance UID of the display set that this display set references.
+ * This is used to determine if the display set is a referenced display set.
+ * It usually is for SEG, RTSTRUCT, etc.
+ */
+ referencedDisplaySetInstanceUID?: string;
+
SeriesDate?: string;
SeriesTime?: string;
instance?: InstanceMetadata;
+
+ isHydrated?: boolean;
+ isRehydratable?: boolean;
};
export type DisplaySetSeriesMetadataInvalidatedEvent = {
diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/commands.md b/platform/docs/docs/migration-guide/3p10-to-3p11/commands.md
new file mode 100644
index 000000000..d38b52f52
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p10-to-3p11/commands.md
@@ -0,0 +1,72 @@
+---
+sidebar_position: 4
+sidebar_label: Commands
+summary: Migration guide for OHIF 3.11's different commands
+---
+
+
+
+## updateStoredPositionPresentation
+
+now uses displaySetInstanceUIDs instead of displaySetInstanceUID as a parameter.
+
+
+## `loadSRMeasurements` Command
+
+**Key Changes:**
+
+* The `loadSRMeasurements` command, previously part of the `@ohif/extension-cornerstone-dicom-sr` extension, has been **removed**.
+* Its functionality of hydrating a Structured Report (SR) and displaying its referenced series in a viewport is now primarily handled by the new `hydrateSecondaryDisplaySet` command available in the `@ohif/extension-cornerstone` extension.
+* The `hydrateStructuredReport` command (from `@ohif/extension-cornerstone-dicom-sr`) now solely focuses on hydrating the SR and returning its data, without directly manipulating viewports.
+
+**Migration Steps:**
+
+If you were previously using the `loadSRMeasurements` command to load and display SR measurements, you should update your code to use the `hydrateSecondaryDisplaySet` command.
+
+1. **Identify `loadSRMeasurements` Usage:**
+ Locate where your code calls `commandsManager.runCommand('loadSRMeasurements', ...)`.
+
+2. **Update to `hydrateSecondaryDisplaySet`:**
+ Replace the call to `loadSRMeasurements` with `hydrateSecondaryDisplaySet`. You will need to pass the full `displaySet` object for the SR and the target `viewportId`.
+
+ ```diff
+ - // Old way: using loadSRMeasurements
+ - commandsManager.runCommand('loadSRMeasurements', {
+ - displaySetInstanceUID: srDisplaySetInstanceUID,
+ - // viewportId was implicitly the active one or not directly specifiable here
+ - });
+ -
+ + // New way: using hydrateSecondaryDisplaySet
+ + const { displaySetService, viewportGridService } = servicesManager.services;
+ +
+ + // 1. Get the SR displaySet object
+ + const srDisplaySet = displaySetService.getDisplaySetByUID(srDisplaySetInstanceUID);
+ +
+ + // 2. Determine the target viewportId (e.g., active viewport)
+ + const viewportId = viewportGridService.getActiveViewportId(); // Or your specific viewportId
+ +
+ + if (srDisplaySet && viewportId) {
+ + commandsManager.runCommand('hydrateSecondaryDisplaySet', {
+ + displaySet: srDisplaySet,
+ + viewportId: viewportId,
+ + });
+ + } else {
+ + console.warn('SR DisplaySet or ViewportId not found, cannot hydrate.');
+ + }
+ ```
+
+**Explanation:**
+
+* The `loadSRMeasurements` command was responsible for both hydrating the SR (getting its measurement data and referenced series UIDs) and then updating the viewport to show the referenced series.
+* The new `hydrateSecondaryDisplaySet` command, when given an SR `displaySet` (`displaySet.Modality === 'SR'`), will:
+ 1. Internally call the `hydrateStructuredReport` command to parse the SR and get its details (including `SeriesInstanceUIDs` of referenced images).
+ 2. Then, it will automatically find the corresponding image display sets for those `SeriesInstanceUIDs`.
+ 3. Finally, it will update the specified `viewportId` to display the primary referenced image series.
+* This change centralizes the logic for hydrating secondary display sets (like SR, SEG, RTSTRUCT) and updating viewports into the `hydrateSecondaryDisplaySet` command within the core Cornerstone extension.
+
+**Note on UI/Button Changes:**
+The UI button typically associated with "Load SR" (often seen in viewport corners or specific contexts) has also been refactored. The hydration of SRs is now often triggered by:
+* The `TrackedMeasurementsContext` if the `@ohif/extension-measurement-tracking` is in use.
+* The new `ModalityLoadBadge` component, which can appear in viewports containing SR, SEG, or RTSTRUCT display sets, offering a "LOAD" action that calls `hydrateSecondaryDisplaySet`.
+
+If you had custom UI invoking `loadSRMeasurements`, you'll need to adapt it to call `hydrateSecondaryDisplaySet` as described above.
diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/hydration.md b/platform/docs/docs/migration-guide/3p10-to-3p11/hydration.md
new file mode 100644
index 000000000..0f82524f4
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p10-to-3p11/hydration.md
@@ -0,0 +1,85 @@
+---
+title: Hydration
+sidebar_position: 3
+sidebar_label: Hydration
+summary: Migration guide for OHIF 3.11's hydration system changes, including the transition to a centralized hydration dialog and command-based hydration for secondary display sets.
+---
+
+## Update Hydration Logic:
+ * The `promptHydrateSEG` and `promptHydrateRT` functions have been updated to use the generic `utils.promptHydrationDialog` from `@ohif/extension-cornerstone`.
+ * The actual hydration is now often triggered by the `hydrateSecondaryDisplaySet` command.
+
+ *Example: `promptHydrateRT` change*
+
+ ```diff
+ // extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts
+ - const RESPONSE = {
+ - NO_NEVER: -1,
+ - CANCEL: 0,
+ - HYDRATE_SEG: 5,
+ - };
+ + import { utils, Types } from '@ohif/extension-cornerstone';
+
+ function promptHydrateRT({
+ servicesManager,
+ rtDisplaySet,
+ viewportId,
+ preHydrateCallbacks,
+ hydrateRTDisplaySet,
+ -}: withAppTypes) {
+ - const { uiViewportDialogService, customizationService } = servicesManager.services;
+ - // ... lots of old promise and dialog logic
+ - return new Promise(async function (resolve, reject) {
+ - // ...
+ - });
+ -}
+ -
+ -function _askHydrate(
+ - // ...
+ -) {
+ - // ...
+ -}
+ +}: {
+ + servicesManager: AppTypes.ServicesManager;
+ + rtDisplaySet: AppTypes.DisplaySet;
+ + viewportId: string;
+ + preHydrateCallbacks?: Types.HydrationCallback[];
+ + hydrateRTDisplaySet: Types.HydrationCallback;
+ +}) {
+ + return utils.promptHydrationDialog({
+ + servicesManager,
+ + viewportId,
+ + displaySet: rtDisplaySet,
+ + preHydrateCallbacks,
+ + hydrateCallback: hydrateRTDisplaySet,
+ + type: 'RTSTRUCT',
+ + });
+ }
+ ```
+ The `hydrateRTDisplaySet` callback passed to this function would now typically involve the `hydrateSecondaryDisplaySet` command.
+ ```diff
+ // extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx
+ useEffect(() => {
+ if (rtIsLoading) {
+ return;
+ }
+ promptHydrateRT({
+ servicesManager,
+ viewportId,
+ rtDisplaySet,
+ - preHydrateCallbacks: [storePresentationState],
+ - hydrateRTDisplaySet,
+ - }).then(isHydrated => {
+ - if (isHydrated) {
+ - setIsHydrated(true);
+ - }
+ + hydrateRTDisplaySet: async () => {
+ + return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
+ + displaySet: rtDisplaySet,
+ + viewportId,
+ + });
+ + },
+ });
+ - }, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
+ + }, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager]);
+ ```
diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/index.md b/platform/docs/docs/migration-guide/3p10-to-3p11/index.md
index bee159b84..d60fdfd76 100644
--- a/platform/docs/docs/migration-guide/3p10-to-3p11/index.md
+++ b/platform/docs/docs/migration-guide/3p10-to-3p11/index.md
@@ -6,17 +6,3 @@ sidebar_label: 3.10 -> 3.11 beta
# Migration Guide
This guide provides information about migrating from OHIF version 3.10 to version 3.11.
-
-## General
-
-`viewportActionMenu.segmentationOverlay` is renamed to `viewportActionMenu.dataOverlay`
-as it handles now both segmentation and data overlay.
-
-## Viewport Action Menu Customization
-
-The structure for defining viewport action menu customizations has changed. See the [Viewport Action Menu](./viewport-action-menu.md) migration guide for details.
-
-
-## updateStoredPositionPresentation
-
-now uses displaySetInstanceUIDs instead of displaySetInstanceUID.
diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/toolbarService.md b/platform/docs/docs/migration-guide/3p10-to-3p11/toolbarService.md
new file mode 100644
index 000000000..510ce538b
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p10-to-3p11/toolbarService.md
@@ -0,0 +1,153 @@
+---
+sidebar_position: 2
+sidebar_label: Toolbar Service
+summary: Migration guide for OHIF 3.11's toolbar service changes, including the transition from `ViewportActionCornersService` to `ToolbarService`
+---
+
+
+**Key Changes:**
+
+* **`ViewportActionCornersService` Removed:** The `ViewportActionCornersService` and its associated provider (`ViewportActionCornersProvider`) and hook (`useViewportActionCorners`) have been removed. Functionality for viewport corner items is now integrated into the `ToolbarService` and standard toolbar components.
+* **Viewport Corner Items as Toolbar Buttons:** Items previously managed by `ViewportActionCornersService` are now configured as regular toolbar buttons. They are assigned to specific toolbar sections (e.g., `viewportActionMenu.topLeft`) and rendered using `Toolbar` components within the viewport corners.
+* **`ToolbarService` API Updates:**
+ * `ToolbarService.addButtons()` has been renamed to `ToolbarService.register()` to better reflect its purpose of registering button definitions rather than just adding them.
+ * `ToolbarService.createButtonSection()` has been renamed to `ToolbarService.updateSection()` to better reflect that it is not about creating new sections but updating existing ones.
+ * `ToolbarService` now has a `sections` property (e.g., `toolbarService.sections.viewportActionMenu.topLeft`) providing predefined section names.
+* **Enhanced `useToolbar` Hook:**
+ * The `useToolbar` hook now returns additional state management functions for toolbar items:
+ * `openItem`, `closeItem`, `isItemOpen` (for managing dropdown/popover states).
+ * `lockItem`, `unlockItem`, `toggleLock`, `isItemLocked`.
+ * `showItem`, `hideItem`, `toggleVisibility`, `isItemVisible`.
+ * The `onInteraction` callback now receives `itemId` and `viewportId`.
+* **Toolbar Button Configuration:**
+ * The `groupId` prop in button configurations (e.g., for `ohif.toolButtonList`, `ohif.toolBoxButtonGroup`) is generally replaced by directly using `buttonSection` to define the set of buttons.
+ * Button `evaluate` functions can now leverage `evaluateProps.hideWhenDisabled` to automatically hide a button when it's disabled.
+* **New UI Components & Hooks for Viewport Corners:**
+ * Specialized components like `ModalityLoadBadge`, `NavigationComponent`, `TrackingStatus`, `ViewportDataOverlayMenuWrapper`, `ViewportOrientationMenuWrapper`, `WindowLevelActionMenuWrapper` are now used as toolbar buttons, typically in viewport action menu sections.
+ * `useViewportHover` hook can be used to determine if a viewport is hovered or active, controlling the visibility of corner toolbars.
+* **`IconPresentationProvider`:** A new `IconPresentationProvider` and `useIconPresentation` hook have been introduced to standardize icon sizing and styling within toolbars and related components.
+* **Legacy Toolbar Components Removed:** `ToolbarSplitButtonWithServicesLegacy` and `ToolbarButtonGroupWithServicesLegacy` have been removed.
+
+**Migration Steps:**
+
+1. **Update `ToolbarService` Method Calls:**
+ * Replace all instances of `toolbarService.addButtons(...)` with `toolbarService.register(...)`.
+ * Replace all instances of `toolbarService.createButtonSection(...)` with `toolbarService.updateSection(...)`.
+
+ ```diff
+ // Before
+ - toolbarService.addButtons(toolbarButtons);
+ - toolbarService.createButtonSection('primary', ['Zoom', 'Pan']);
+
+ // After
+ + toolbarService.register(toolbarButtons);
+ + toolbarService.updateSection('primary', ['Zoom', 'Pan']);
+ ```
+
+2. **Migrate Viewport Action Corner Items:**
+ * Remove any direct usage of the old `ViewportActionCornersService`, `useViewportActionCorners`, or `ViewportActionCornersProvider`.
+ * Define your viewport corner items (like orientation menu, W/L menu, data overlay menu) as standard toolbar buttons using `toolbarService.register()`.
+ * Assign these buttons to the new dedicated viewport action menu sections. You can access these section names via `toolbarService.sections.viewportActionMenu.`, e.g., `toolbarService.sections.viewportActionMenu.topLeft`.
+
+ ```diff
+ // Before: Customization in viewportActionMenuCustomizations.ts (now deleted)
+ // or direct use of ViewportActionCornersService.addComponent
+ - // Example: viewportActionCornersService.addComponent({ viewportId, id: 'orientationMenu', component: MyOrientationMenu, location: 'topLeft' });
+
+ // After: In your mode's onModeEnter or similar setup
+ + const myViewportCornerButtons = [
+ + {
+ + id: 'orientationMenu',
+ + uiType: 'ohif.orientationMenu', // Or your custom component registered as a UI type
+ + props: { /* ... props for your component ... */ }
+ + },
+ + // ... other corner buttons
+ + ];
+ + toolbarService.register(myViewportCornerButtons);
+ + toolbarService.updateSection(
+ + toolbarService.sections.viewportActionMenu.topLeft,
+ + ['orientationMenu', /* other button IDs */]
+ + );
+ ```
+ * The `OHIFViewportActionCorners.tsx` component now internally uses `Toolbar` components for each corner, which are populated by these sections.
+ * For custom components that act as menus (e.g., popovers), use the `onOpen`, `onClose`, `isOpen` props passed down by the `Toolbar` component (which get these from `useToolbar`).
+
+ ```diff
+ // Before: Custom component might have managed its own open state
+ - // const [isMenuOpen, setIsMenuOpen] = useState(false);
+ - // const handleOpenChange = (open) => setIsMenuOpen(open);
+
+ // After: Custom component receives isOpen, onOpen, onClose from Toolbar
+ + function MyCustomMenuButton({ isOpen, onOpen, onClose, ...rest }) {
+ + const handleOpenChange = (openState: boolean) => {
+ + if (openState) {
+ + onOpen?.();
+ + } else {
+ + onClose?.();
+ + }
+ + };
+ +
+ + return (
+ +
+ + {/* ... PopoverTrigger and PopoverContent ... */}
+ +
+ + );
+ + }
+ ```
+
+3. **Adapt Toolbar Button and Component Configurations:**
+ * For `ohif.toolButtonList` or `ohif.toolBoxButtonGroup` (and their wrappers), the `groupId` prop is no longer the primary way to define the set of buttons. Instead, ensure the `buttonSection` prop correctly points to the section name containing the desired buttons. The `id` prop on these wrapper components should be unique for the component instance.
+
+ ```diff
+ // Before
+ - {
+ - id: 'MeasurementTools',
+ - uiType: 'ohif.toolButtonList',
+ - props: {
+ - buttonSection: 'measurementSection',
+ - groupId: 'MeasurementTools', // groupId often matched buttonSection
+ - },
+ - },
+
+ // After
+ + {
+ + id: 'MeasurementTools', // This is the ID of the ToolButtonList/ToolBox component itself
+ + uiType: 'ohif.toolButtonList',
+ + props: {
+ + // This section contains the actual tool buttons (e.g., Length, Bidirectional)
+ + buttonSection: 'measurementSection',
+ + },
+ + },
+ ```
+ * Update wrappers like `ToolBoxButtonGroupWrapper` and `ToolButtonListWrapper`:
+ * The `groupId` prop is replaced by `id` (which is the ID of the wrapper button itself).
+ * The `onInteraction` callback in these wrappers now provides `id` (the wrapper's ID) instead of `groupId`.
+ * If you have custom `evaluate` functions, you can now use `evaluateProps: { hideWhenDisabled: true }` in your button definition to automatically hide the button if it evaluates to disabled.
+
+
+5. **Adopt `IconPresentationProvider` (Optional but Recommended):**
+ * For consistent icon styling across your application's toolbars, wrap a high-level component (like your main `Header` or layout component) with ``.
+ * Custom tool button components can then use the `useIconPresentation` hook to get appropriate class names for icons or a pre-styled `IconContainer`.
+
+ ```diff
+ // In your main App.tsx or Header.tsx
+ + import { IconPresentationProvider, ToolButton } from '@ohif/ui-next';
+ // ...
+ +
+ {/* Your Header content including Toolbars */}
+ +
+
+ // In a custom tool button using icons
+ + import { useIconPresentation, Icons } from '@ohif/ui-next';
+ + function MyCustomToolButton({ iconName }) {
+ + const { className: iconClassName } = useIconPresentation();
+ + return ;
+ + }
+ ```
+
+6. **Remove Legacy Component Usage:**
+ * Replace any usage of `ToolbarSplitButtonWithServicesLegacy` and `ToolbarButtonGroupWithServicesLegacy` with the newer patterns, typically by configuring individual buttons and using `ToolButtonList` or `ButtonGroup` from `@ohif/ui-next` directly, driven by `useToolbar`.
diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/ui.md b/platform/docs/docs/migration-guide/3p10-to-3p11/ui.md
new file mode 100644
index 000000000..074790e5d
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p10-to-3p11/ui.md
@@ -0,0 +1,60 @@
+---
+sidebar_position: 5
+sidebar_label: UI
+summary: Migration guide for OHIF 3.11's UI changes, including the transition from `ViewportActionCornersService` to `ToolbarService` and the introduction of `useToolbar` hook.
+---
+
+
+## New useIconPresentation hook
+
+This section details the introduction of the `IconPresentationProvider` and `useIconPresentation` hook, offering an optional way to manage icon size and container styling within the UI.
+
+### Key Changes:
+
+* **New Context and Hook:** Introduction of `IconPresentationProvider` and `useIconPresentation` to provide a standardized way to control the size and potentially the container component/props for icons and related interactive elements (like `ToolButton`).
+
+### Migration Steps:
+
+Using the `IconPresentationProvider` is **entirely optional**. If you do not wrap your components with this provider, components like `ToolButton` will continue to use their explicit `size` prop and default styling.
+
+However, if you wish to centrally manage the presentation of icons within a specific part of your application's component tree, follow these steps:
+
+1. **Identify the component subtree:** Determine which section of your UI you want to apply consistent icon styling to.
+
+2. **Wrap with `IconPresentationProvider`:** Wrap the root of that component subtree with the `IconPresentationProvider`. Pass the desired `size` prop. You can also optionally provide a custom `IconContainer` component and `containerProps` if you want to change the wrapper around the icon itself (e.g., switching from a `Button` to a `ToolButton` or applying specific styling).
+
+ ```jsx
+ import { IconPresentationProvider, ToolButton } from '@ohif/ui-next';
+
+ function MyComponentTree() {
+ return (
+ // Icons and ToolButtons within this provider will inherit 'large' size
+
+ {/* Any components inside that consume the context */}
+
+ {/* ... other components ... */}
+
+ );
+ }
+ ```
+
+3. **Consume the context in components (if building custom components):** If you are building a custom component that renders an icon and want it to respect the provider's settings, use the `useIconPresentation` hook within that component. This hook provides the configured size, a calculated CSS class name (`className`), the specified `IconContainer` component, and its `containerProps`.
+
+ ```jsx
+ import React from 'react';
+ import { useIconPresentation, Icons } from '@ohif/ui-next';
+
+ function MyCustomIconButton({ iconName, ...rest }) {
+ // This hook reads the nearest IconPresentationProvider context
+ const { className, IconContainer, containerProps } = useIconPresentation();
+
+ // Use the provided IconContainer and its props
+ return (
+
+ {/* Use the calculated className for the icon */}
+
+
+ );
+ }
+ ```
+ *Note: Built-in components like `ToolButton` in `@ohif/ui-next` have been updated internally to consume this context automatically if a provider is available.*
diff --git a/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md b/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md
index 2b7f36d98..b2b1fd307 100644
--- a/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md
+++ b/platform/docs/docs/migration-guide/3p10-to-3p11/viewport-action-menu.md
@@ -1,102 +1,124 @@
---
-sidebar_position: 2
-sidebar_label: Viewport Action Menu
-summary: Migration guide for OHIF 3.11's viewport action menu customization changes, including the transition from individual item configurations to location-based arrays and the removal of index priorities.
+sidebar_position: 1
+sidebar_label: Viewport Corners
+summary: Migration guide for OHIF 3.11's viewport corners customization changes, including the transition from individual item configurations to location-based arrays and the removal of index priorities.
---
-# Viewport Action Menu Customization
-In OHIF 3.11, we've redesigned how viewport action menu customizations are defined to make them more intuitive and organized by location.
+Okay, here's a migration guide based on the provided diff, focusing on the introduction of `TrackingStatus`, `ModalityLoadBadge`, and `NavigationComponent`.
-## Changes
+**Key Changes:**
-Previously, viewport action menu customizations were defined with individual item configurations that specified location and priority:
+* **Deprecated `ViewportActionCornersService`**: The `ViewportActionCornersService` and its associated provider (`ViewportActionCornersProvider`) have been removed. UI elements previously managed by this service are now typically handled by dedicated components integrated via the `ToolbarService`.
+* **New Centralized UI Components**:
+ * `ModalityLoadBadge`: A new component in `@ohif/extension-cornerstone` that displays the status (e.g., SEG/RT/SR loaded or requiring hydration) and a "LOAD" button for secondary display sets (SEG, RTSTRUCT, SR). This replaces the inline status and load logic within individual viewport components like `OHIFCornerstoneSEGViewport` and `OHIFCornerstoneRTViewport`.
+ * `TrackingStatus`: A new component in `@ohif/extension-cornerstone` to indicate if measurements in a viewport are being tracked. This replaces inline tracking status indicators previously in `OHIFCornerstoneSRMeasurementViewport` and `TrackedCornerstoneViewport`.
+ * `NavigationComponent`: A new component in `@ohif/extension-cornerstone` that provides navigation arrows (e.g., for segments in SEG/RT or measurements in SR/tracked series). This replaces the `ViewportActionArrows` previously instantiated directly within viewport components.
+* **Viewport Simplification**: Viewport components like `OHIFCornerstoneSEGViewport`, `OHIFCornerstoneRTViewport`, and `OHIFCornerstoneSRMeasurementViewport` have been simplified. They no longer manage their own status indicators, load buttons, or navigation arrows. They now primarily delegate rendering to `OHIFCornerstoneViewport`.
+* **Refactored Hydration Prompts**: Utility functions like `promptHydrateSEG` and `promptHydrateRT` now use a centralized `utils.promptHydrationDialog` from `@ohif/extension-cornerstone`.
+* **Centralized Hydration Command**: A new command `hydrateSecondaryDisplaySet` has been added to `@ohif/extension-cornerstone` to handle the hydration logic for SEG, RTSTRUCT, and SR display sets.
+* **New Hooks**: Several new hooks have been introduced in `@ohif/extension-cornerstone` (e.g., `useViewportDisplaySets`, `useMeasurementTracking`, `useViewportSegmentations`, `useViewportHover`) to provide data and state for these new UI components.
-```ts
-export default {
- 'viewportActionMenu.orientationMenu': {
- enabled: true,
- location: viewportActionCornersService.LOCATIONS.topLeft,
- indexPriority: 1,
- },
- 'viewportActionMenu.dataOverlay': {
- enabled: true,
- location: viewportActionCornersService.LOCATIONS.topLeft,
- indexPriority: 2,
- },
- // ...
-};
-```
+**Migration Steps:**
-Now, viewport action menu customizations are organized by location, with each location having its own customization ID:
+1. **Remove `ViewportActionCornersService` Usage**:
+ * If you were using `ViewportActionCornersService` to add custom components to viewport corners, you will need to refactor this. The recommended approach is to define these components as toolbar buttons and place them in designated viewport action menu sections (e.g., `viewportActionMenu.topLeft`) using the `ToolbarService`.
+ * The internal status components (`_getStatusComponent`) and `ViewportActionArrows` within specific viewports (SEG, RT, SR) have been removed. Their functionality is now provided by `ModalityLoadBadge`, `TrackingStatus`, and `NavigationComponent`.
-```ts
-export default {
- 'viewportActionMenu.topLeft': [
- {
- id: 'orientationMenu',
- enabled: true,
- },
- {
- id: 'dataOverlay',
- enabled: true,
- },
- {
- id: 'windowLevelActionMenu',
- enabled: true,
- },
- ],
- 'viewportActionMenu.topRight': [],
- 'viewportActionMenu.bottomLeft': [],
- 'viewportActionMenu.bottomRight': [],
-};
-```
-## Migration Steps
+3. **Integrate New UI Components via `ToolbarService`**:
+ * The `ModalityLoadBadge`, `TrackingStatus`, and `NavigationComponent` are now registered with the `ToolbarService` within the `@ohif/extension-cornerstone`'s `getToolbarModule`.
+ * Modes (e.g., `longitudinal`) should define toolbar sections for viewport corners and add these components to those sections.
-1. Reorganize your viewport action menu customizations by using location-based customization IDs (`viewportActionMenu.topLeft`, `viewportActionMenu.topRight`, etc.).
-2. For each component, move it into the appropriate location array.
-3. Replace the component key with an `id` property that doesn't include the prefix (just use `orientationMenu` instead of `viewportActionMenu.orientationMenu`).
-4. Remove the `location` property (since it's now implied by the customization ID).
-5. Remove the `indexPriority` property (order in the array now determines display order).
-6. For each component, provide a `component` function that returns the component instance.
+ *Example: Adding components to viewport corners in `longitudinal` mode*
+ ```diff
+ // modes/longitudinal/src/index.ts
+ function modeFactory({ modeConfiguration }) {
+ return {
+ // ...
+ onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
+ // ...
+ toolbarService.addButtons(toolbarButtons);
+ toolbarService.createButtonSection('primary', [
+ // ... primary tools
+ ]);
-## Component Rendering
+ + toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topLeft, [
+ + 'orientationMenu',
+ + 'dataOverlayMenu',
+ + 'windowLevelMenu',
+ + ]);
+ + toolbarService.updateSection(toolbarService.sections.viewportActionMenu.topRight, [
+ + 'modalityLoadBadge',
+ + 'trackingStatus',
+ + 'navigationComponent',
+ + ]);
+ // ...
+ },
+ ```
+ And ensure these buttons are defined in your mode's `toolbarButtons.ts`:
+ ```diff
+ // modes/longitudinal/src/toolbarButtons.ts
+ + {
+ + id: 'modalityLoadBadge',
+ + uiType: 'ohif.modalityLoadBadge',
+ + props: {
+ + // ... props like icon, label, tooltip, evaluate
+ + evaluate: {
+ + name: 'evaluate.modalityLoadBadge',
+ + hideWhenDisabled: true,
+ + },
+ + },
+ + },
+ + {
+ + id: 'navigationComponent',
+ + uiType: 'ohif.navigationComponent',
+ + props: {
+ + // ... props
+ + evaluate: {
+ + name: 'evaluate.navigationComponent',
+ + hideWhenDisabled: true,
+ + },
+ + },
+ + },
+ + {
+ + id: 'trackingStatus',
+ + uiType: 'ohif.trackingStatus',
+ + props: {
+ + // ... props
+ + evaluate: {
+ + name: 'evaluate.trackingStatus',
+ + hideWhenDisabled: true,
+ + },
+ + },
+ + },
+ ```
-Component rendering logic is now included directly in the item configuration via a `component` function:
-```ts
-const createOrientationMenu = ({ viewportId, element, location }) => {
- return getViewportOrientationMenu({
- viewportId,
- element,
- location,
- });
-};
+5. **Direct Import of `OHIFCornerstoneViewport`**:
+ * Extensions that were previously getting the cornerstone viewport component dynamically via `extensionManager.getModuleEntry('@ohif/extension-cornerstone.viewportModule.cornerstone')` should now import `OHIFCornerstoneViewport` directly from `@ohif/extension-cornerstone`.
-const createDataOverlay = ({ viewportId, element, displaySets, location }) => {
- return getViewportDataOverlaySettingsMenu({
- viewportId,
- element,
- displaySets,
- location,
- });
-};
+ ```diff
+ // extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx
+ import PropTypes from 'prop-types';
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
+ import { useViewportGrid } from '@ohif/ui-next';
+ +import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
-export default {
- 'viewportActionMenu.topLeft': [
- {
- id: 'orientationMenu',
- enabled: true,
- component: createOrientationMenu,
- },
- {
- id: 'dataOverlay',
- enabled: true,
- component: createDataOverlay,
- },
- // other components...
- ],
- // other locations...
-};
-```
+ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
+ // ...
+ const getCornerstoneViewport = useCallback(() => {
+ - const { component: Component } = extensionManager.getModuleEntry(
+ - '@ohif/extension-cornerstone.viewportModule.cornerstone'
+ - );
+ // ...
+ return (
+ -
+ + />
+ );
+ // ...
+ ```
diff --git a/platform/docs/docs/platform/hooks/index.md b/platform/docs/docs/platform/hooks/index.md
new file mode 100644
index 000000000..52cc5ee47
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/index.md
@@ -0,0 +1,33 @@
+---
+title: Hooks
+summary: List of React hooks available in the platform, these are custom hooks that are used to access the state of the platform
+---
+
+# Hooks
+
+## [useMeasurements](./useMeasurements.md)
+A React hook that provides mapped measurements from the measurement service with automatic updates when measurements change.
+
+## [useViewportSegmentations](./useViewportSegmentations.md)
+A React hook that provides segmentation data and representations for the active viewport with automatic updates when segmentations change.
+
+## [useMeasurementTracking](./useMeasurementTracking.md)
+A React hook that provides measurement tracking information for a specific viewport, including tracking state and tracked measurement UIDs.
+
+## [useViewportDisplaySets](./useViewportDisplaySets.md)
+A React hook that provides access to display sets associated with a viewport, including background, foreground, overlay, and potential display sets.
+
+## [useViewportHover](./useViewportHover.md)
+A React hook that tracks mouse hover state and active status for a specific viewport.
+
+## [usePatientInfo](./usePatientInfo.md)
+A React hook that provides patient information from the active display sets and detects when multiple patients are loaded.
+
+## [useSearchParams](./useSearchParams.md)
+A React hook that provides access to URL search parameters from both the query string and hash fragment.
+
+## [useDynamicMaxHeight](./useDynamicMaxHeight.md)
+A React hook that calculates the maximum height for an element based on its position in the viewport, with automatic recalculation on window resize or data changes.
+
+## [useSessionStorage](./useSessionStorage.md)
+A React hook that provides sessionStorage access with automatic JSON parsing/stringifying and an option to clear data when the page unloads.
\ No newline at end of file
diff --git a/platform/docs/docs/platform/hooks/useDynamicMaxHeight.md b/platform/docs/docs/platform/hooks/useDynamicMaxHeight.md
new file mode 100644
index 000000000..95854c173
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useDynamicMaxHeight.md
@@ -0,0 +1,65 @@
+---
+title: useDynamicMaxHeight
+summary: A React hook that calculates the maximum height for an element based on its position in the viewport, with automatic recalculation on window resize or data changes.
+---
+
+# useDynamicMaxHeight
+
+The `useDynamicMaxHeight` hook calculates the maximum height an element can have based on its position relative to the bottom of the viewport, ensuring it doesn't overflow or get cut off by the viewport edge.
+
+## Overview
+
+This hook is useful for creating responsive UI elements that need to fit within the visible area of the screen without causing scrolling or content overflow. It automatically recalculates the maximum height when the window is resized or when specified data changes, ensuring the element always fits properly.
+
+## Import
+
+```js
+import { useDynamicMaxHeight } from '@ohif/ui-next';
+```
+
+## Usage
+
+```jsx
+function DynamicHeightPanel({ data, children }) {
+ const { ref, maxHeight } = useDynamicMaxHeight(data, 30, 200);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+## Parameters
+
+- `data` (required): Any data that, when changed, should trigger a recalculation of the maximum height. This can be an array, object, or primitive value.
+- `buffer` (optional): Additional space (in pixels) to leave below the element. Defaults to 20px.
+- `minHeight` (optional): Minimum height (in pixels) for the element. Defaults to 100px.
+
+## Returns
+
+An object containing:
+
+- `ref`: A React ref object that must be attached to the target DOM element
+- `maxHeight`: A CSS-compatible string value for the calculated maximum height (e.g., "500px")
+
+## Implementation Details
+
+- The hook uses `window.innerHeight` and the element's position to calculate the available space between the element's top edge and the bottom of the viewport.
+- It subtracts the specified buffer from the available height to ensure there's space below the element.
+- The calculated height is constrained by the specified minimum height to prevent the element from becoming too small.
+- The hook uses `requestAnimationFrame` to ensure the initial calculation happens after the component has been rendered and positioned in the DOM.
+- It automatically recalculates the maximum height when:
+ - The window is resized
+ - The `data`, `buffer`, or `minHeight` dependencies change
+- The hook properly cleans up event listeners and animation frame requests when the component unmounts.
+
+This hook is particularly useful for panels, menus, and content areas that need to dynamically adjust their height based on their position in the viewport, ensuring a good user experience without unexpected scrolling or content clipping.
\ No newline at end of file
diff --git a/platform/docs/docs/platform/hooks/useMeasurementTracking.md b/platform/docs/docs/platform/hooks/useMeasurementTracking.md
new file mode 100644
index 000000000..b488e86d0
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useMeasurementTracking.md
@@ -0,0 +1,79 @@
+---
+title: useMeasurementTracking
+summary: A React hook that provides measurement tracking information for a specific viewport, including tracking state and tracked measurement UIDs.
+---
+
+# useMeasurementTracking
+
+The `useMeasurementTracking` hook provides measurement tracking information for a specific viewport, including the tracking state and UIDs of tracked measurements associated with the viewport's series.
+
+## Overview
+
+This hook gives components access to tracking information for measurements in a viewport. It monitors the tracked measurements service and measurement service to provide up-to-date tracking states and the list of measurement UIDs associated with the series displayed in the viewport.
+
+## Import
+
+```js
+import { useMeasurementTracking } from '@ohif/extension-cornerstone';
+```
+
+## Usage
+
+```jsx
+function MeasurementTrackingInfo({ viewportId }) {
+ const {
+ isTracked,
+ isLocked,
+ seriesInstanceUID,
+ trackedMeasurementUIDs
+ } = useMeasurementTracking({
+ viewportId,
+ });
+
+ return (
+
+
Series UID: {seriesInstanceUID}
+
Tracking Status: {isTracked ? 'Tracked' : 'Not Tracked'}
+
Locked: {isLocked ? 'Yes' : 'No'}
+
Tracked Measurements: {trackedMeasurementUIDs.length}
+
+ {trackedMeasurementUIDs.map(uid => (
+ {uid}
+ ))}
+
+
+ );
+}
+```
+
+## Parameters
+
+- `options` - Configuration options:
+ - `viewportId` (required): The ID of the viewport to track
+
+## Returns
+
+An object containing the following properties:
+
+- `isTracked`: Boolean indicating if the series in the viewport is currently tracked
+- `isLocked`: Boolean indicating if tracking is enabled (locked) globally
+- `seriesInstanceUID`: The Series Instance UID of the background display set in the viewport
+- `trackedMeasurementUIDs`: Array of measurement UIDs that are associated with the tracked series in the viewport
+
+## Events
+
+The hook automatically updates when any of these events occur:
+
+From the tracked measurements service:
+- `TRACKING_ENABLED`
+- `TRACKING_DISABLED`
+- `TRACKED_SERIES_CHANGED`
+- `SERIES_ADDED`
+- `SERIES_REMOVED`
+
+From the measurement service:
+- `MEASUREMENT_ADDED`
+- `RAW_MEASUREMENT_ADDED`
+- `MEASUREMENT_UPDATED`
+- `MEASUREMENT_REMOVED`
+- `MEASUREMENTS_CLEARED`
diff --git a/platform/docs/docs/platform/hooks/useMeasurements.md b/platform/docs/docs/platform/hooks/useMeasurements.md
new file mode 100644
index 000000000..d1336d0c7
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useMeasurements.md
@@ -0,0 +1,68 @@
+---
+title: useMeasurements
+summary: A React hook that provides mapped measurements from the measurement service with automatic updates when measurements change.
+---
+
+# useMeasurements
+
+The `useMeasurements` hook provides access to measurements from the measurement service, with automatic updates when measurements are added, updated, or removed.
+
+## Overview
+
+This hook retrieves measurements from the measurement service and maps them to a display-friendly format. It monitors various measurement service events and updates the returned measurements automatically when changes occur.
+
+## Import
+
+```js
+import { useMeasurements } from '@ohif/extension-cornerstone';
+```
+
+## Usage
+
+```jsx
+function MeasurementPanel() {
+ const measurementFilter = measurements => measurements.someFilter;
+
+ const measurements = useMeasurements({
+ measurementFilter,
+ });
+
+ return (
+
+ {measurements.map(measurement => (
+
+
{measurement.label}
+
+ {measurement.displayText.primary.map((text, i) => (
+
{text}
+ ))}
+
+
+ ))}
+
+ );
+}
+```
+
+## Parameters
+
+- `options` - Configuration options:
+ - `measurementFilter` - Optional function to filter measurements returned by the measurement service.
+
+## Returns
+
+An array of mapped measurements with the following structure:
+
+
+## Events
+
+The hook automatically updates when any of these measurement service events occur:
+- `MEASUREMENT_ADDED`
+- `RAW_MEASUREMENT_ADDED`
+- `MEASUREMENT_UPDATED`
+- `MEASUREMENT_REMOVED`
+- `MEASUREMENTS_CLEARED`
+
+## Implementation Details
+
+The hook uses debouncing to prevent excessive re-renders when multiple measurement events occur in rapid succession. It also performs a deep comparison of the measurements to avoid unnecessary state updates when the data hasn't actually changed.
diff --git a/platform/docs/docs/platform/hooks/usePatientInfo.md b/platform/docs/docs/platform/hooks/usePatientInfo.md
new file mode 100644
index 000000000..62bc9b9ae
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/usePatientInfo.md
@@ -0,0 +1,68 @@
+---
+title: usePatientInfo
+summary: A React hook that provides patient information from the active display sets and detects when multiple patients are loaded.
+---
+
+# usePatientInfo
+
+The `usePatientInfo` hook provides access to basic patient demographic information from the active display sets and also detects when display sets from multiple patients are loaded simultaneously.
+
+## Overview
+
+This hook retrieves patient information from the first instance of the first display set added to the viewer. It monitors when new display sets are added and updates the patient information accordingly. It also checks if any of the active display sets are from different patients and provides this information through the `isMixedPatients` flag.
+
+## Import
+
+```js
+import { usePatientInfo } from '@ohif/extension-default';
+```
+
+## Usage
+
+```jsx
+function PatientBanner() {
+ const { patientInfo, isMixedPatients } = usePatientInfo();
+
+ return (
+
+ {isMixedPatients && (
+
Multiple patients loaded
+ )}
+
{patientInfo.PatientName}
+
+ ID: {patientInfo.PatientID}
+ Sex: {patientInfo.PatientSex}
+ DOB: {patientInfo.PatientDOB}
+
+
+ );
+}
+```
+
+## Parameters
+
+This hook doesn't take any parameters.
+
+## Returns
+
+An object containing:
+
+- `patientInfo`: Object with the following properties:
+ - `PatientName`: Formatted patient name
+ - `PatientID`: Patient identifier
+ - `PatientSex`: Patient sex
+ - `PatientDOB`: Formatted patient date of birth
+- `isMixedPatients`: Boolean indicating whether multiple patients are loaded in the viewer
+
+## Events
+
+The hook subscribes to the following display set service events:
+
+- `DISPLAY_SETS_ADDED`: Updates patient information when new display sets are added to the viewer
+
+## Implementation Details
+
+- Patient name and date of birth are formatted using OHIF utility functions.
+- The hook checks all active display sets to determine if they belong to different patients.
+- Patient information is initialized with empty strings and updated when display sets are added.
+- When no instances are available in a display set, the hook attempts to get information from the `instance` property as a fallback.
\ No newline at end of file
diff --git a/platform/docs/docs/platform/hooks/useSearchParams.md b/platform/docs/docs/platform/hooks/useSearchParams.md
new file mode 100644
index 000000000..909c1b0bc
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useSearchParams.md
@@ -0,0 +1,69 @@
+---
+title: useSearchParams
+summary: A React hook that provides access to URL search parameters from both the query string and hash fragment.
+---
+
+# useSearchParams
+
+The `useSearchParams` hook provides access to URL search parameters, combining both query string parameters and hash fragment parameters into a single URLSearchParams object.
+
+## Overview
+
+This hook extends the standard React Router `useLocation` functionality by merging search parameters from both the query string and the hash fragment of the URL. It also provides an option to normalize parameter keys to lowercase for case-insensitive parameter handling.
+
+## Import
+
+```js
+import { useSearchParams } from '@ohif/app';
+```
+
+## Usage
+
+```jsx
+function RouteParameterReader() {
+ const searchParams = useSearchParams();
+ // Or with lowercase keys option
+ const lowerCaseParams = useSearchParams({ lowerCaseKeys: true });
+
+ const studyInstanceUID = searchParams.get('StudyInstanceUID');
+ // With lowerCaseKeys: true
+ const sameStudyUID = lowerCaseParams.get('studyinstanceuid');
+
+ return (
+
+
Study UID: {studyInstanceUID}
+
All Parameters:
+
+ {Array.from(searchParams.entries()).map(([key, value]) => (
+
+ {key}: {value}
+
+ ))}
+
+
+ );
+}
+```
+
+## Parameters
+
+- `options` (optional): Configuration options
+ - `lowerCaseKeys` - Boolean indicating whether to convert all parameter keys to lowercase (default: false)
+
+## Returns
+
+A `URLSearchParams` object containing all parameters from both:
+- The query string (e.g., `?param1=value1¶m2=value2`)
+- The hash fragment (e.g., `#param3=value3¶m4=value4`)
+
+If parameters with the same key exist in both the query string and hash fragment, the hash fragment values take precedence.
+
+## Implementation Details
+
+- The hook uses React Router's `useLocation` to access the current URL.
+- It first creates a URLSearchParams object from the location's search property.
+- It then creates another URLSearchParams object from the location's hash property (excluding the leading '#').
+- The parameters from the hash are added to the search parameters, overriding any duplicate keys.
+- If the `lowerCaseKeys` option is enabled, it creates a new URLSearchParams object with all keys converted to lowercase.
+
+This is particularly useful in OHIF where parameters may be specified in either the query string or hash fragment, and where case-sensitivity of parameter names might vary across different systems.
diff --git a/platform/docs/docs/platform/hooks/useSessionStorage.md b/platform/docs/docs/platform/hooks/useSessionStorage.md
new file mode 100644
index 000000000..8eed706d5
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useSessionStorage.md
@@ -0,0 +1,84 @@
+---
+title: useSessionStorage
+summary: A React hook that provides sessionStorage access with automatic JSON parsing/stringifying and an option to clear data when the page unloads.
+---
+
+# useSessionStorage
+
+The `useSessionStorage` hook provides a convenient way to store and retrieve data from the browser's sessionStorage with automatic JSON serialization and deserialization. It also offers the option to automatically clear the stored data when a page unloads.
+
+## Overview
+
+This hook wraps the browser's sessionStorage API to provide a more React-friendly interface. It handles JSON serialization/deserialization automatically and maintains the stored values in local state for reactive updates. The hook also includes a unique feature to clear specific items from sessionStorage when the page unloads, which is useful for temporary session data that shouldn't persist.
+
+## Import
+
+```js
+import { useSessionStorage } from '@ohif/ui-next';
+```
+
+## Usage
+
+```jsx
+function UserPreferencesPanel() {
+ const [preferences, setPreferences] = useSessionStorage({
+ key: 'viewer-preferences',
+ defaultValue: { theme: 'dark', fontSize: 'medium' },
+ clearOnUnload: false,
+ });
+
+ const updateTheme = (theme) => {
+ setPreferences({ ...preferences, theme });
+ };
+
+ return (
+
+
User Preferences
+
Current Theme: {preferences.theme}
+
updateTheme('light')}>Light Theme
+
updateTheme('dark')}>Dark Theme
+
+ );
+}
+
+function TemporaryWorkspace() {
+ const [workspace, setWorkspace] = useSessionStorage({
+ key: 'temp-workspace',
+ defaultValue: { annotations: [] },
+ clearOnUnload: true, // This data will be cleared when the page unloads
+ });
+
+ return (
+
+
Temporary Workspace
+
This workspace will be cleared when you leave the page
+ {/* Workspace UI components */}
+
+ );
+}
+```
+
+## Parameters
+
+An options object with the following properties:
+
+- `key` (required): The sessionStorage key under which to store the data
+- `defaultValue` (optional): The default value to use if no data exists in sessionStorage for the given key. Defaults to an empty object (`{}`)
+- `clearOnUnload` (optional): Whether to clear this item from sessionStorage when the page unloads. Defaults to `false`
+
+## Returns
+
+An array containing:
+
+1. The current value from sessionStorage (parsed from JSON)
+2. A function to update the value in both state and sessionStorage
+
+The update function automatically handles JSON stringification of the data.
+
+## Implementation Details
+
+- The hook uses a global Map (`sessionItemsToClearOnUnload`) to track items that should be cleared when the page unloads.
+- It utilizes the browser's `visibilitychange` event to implement the clearOnUnload feature. When the page becomes hidden (which happens both when switching tabs and when unloading), items marked for clearing are removed from sessionStorage.
+- If the page later becomes visible again (e.g., when switching back to the tab), the items are restored from the Map.
+- The hook's update function merges the new value with the current state using the spread operator, maintaining a similar behavior to React's `setState`.
+- All data is automatically serialized to JSON before storing in sessionStorage and deserialized when retrieving.
diff --git a/platform/docs/docs/platform/hooks/useViewportDisplaySets.md b/platform/docs/docs/platform/hooks/useViewportDisplaySets.md
new file mode 100644
index 000000000..7c3a99f5e
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useViewportDisplaySets.md
@@ -0,0 +1,98 @@
+---
+title: useViewportDisplaySets
+summary: A React hook that provides access to display sets associated with a viewport, including background, foreground, overlay, and potential display sets.
+---
+
+# useViewportDisplaySets
+
+The `useViewportDisplaySets` hook provides access to display sets associated with a viewport, organized into categories based on their role and potential usage.
+
+## Overview
+
+This hook retrieves all display sets associated with a specific viewport and categorizes them into background, foreground, overlays, and potential display sets that could be added to the viewport in different roles. It allows components to efficiently access and manage viewport display sets for various UI interactions like layer menus and display set selectors.
+
+## Import
+
+```js
+import { useViewportDisplaySets } from '@ohif/extension-cornerstone';
+```
+
+## Usage
+
+```jsx
+function ViewportLayerControls({ viewportId }) {
+ const {
+ backgroundDisplaySet,
+ foregroundDisplaySets,
+ overlayDisplaySets,
+ potentialOverlayDisplaySets,
+ potentialForegroundDisplaySets,
+ potentialBackgroundDisplaySets,
+ } = useViewportDisplaySets(viewportId);
+
+ return (
+
+
+
Background
+
{backgroundDisplaySet?.SeriesDescription}
+
+
+
+
Foreground ({foregroundDisplaySets.length})
+ {foregroundDisplaySets.map(ds => (
+
{ds.SeriesDescription}
+ ))}
+
+
+
+
Overlays ({overlayDisplaySets.length})
+ {overlayDisplaySets.map(ds => (
+
{ds.SeriesDescription}
+ ))}
+
+
+
+
Available Overlays ({potentialOverlayDisplaySets.length})
+ {potentialOverlayDisplaySets.map(ds => (
+
{ds.SeriesDescription}
+ ))}
+
+
+ );
+}
+```
+
+## Parameters
+
+- `viewportId` (optional): The ID of the viewport to get display sets for. If not provided, uses the active viewport.
+- `options` (optional): Configuration options to control which display sets to include:
+ - `includeBackground`: Whether to include the background display set (default: true)
+ - `includeForeground`: Whether to include foreground display sets (default: true)
+ - `includeOverlay`: Whether to include overlay display sets (default: true)
+ - `includePotentialOverlay`: Whether to include potential overlay display sets (default: true)
+ - `includePotentialForeground`: Whether to include potential foreground display sets (default: true)
+ - `includePotentialBackground`: Whether to include potential background display sets (default: true)
+
+## Returns
+
+An object containing requested display set collections based on options:
+
+- `allDisplaySets`: All display sets in the viewer (only if requested)
+- `viewportDisplaySets`: The display sets currently in the viewport
+- `backgroundDisplaySet`: The primary display set for the viewport (base image)
+- `foregroundDisplaySets`: Display sets currently shown with background (non-overlay layers)
+- `overlayDisplaySets`: Segmentation display sets currently applied as overlays
+- `potentialOverlayDisplaySets`: Display sets that could be toggled on as overlays (derived modalities)
+- `potentialForegroundDisplaySets`: Display sets that could be added as foreground layers
+- `potentialBackgroundDisplaySets`: Display sets that could replace the current background
+
+Each property is only included if the corresponding option is true.
+
+## Implementation Details
+
+- The hook automatically adapts to viewport changes through the `useViewportGrid` hook.
+- It only fetches and processes display sets that are needed based on the provided options.
+- Display sets are categorized based on their modality and properties.
+- Potential display sets are sorted by priority to present the most relevant options first.
+- Derived overlay modalities (like SEG, SR) are treated differently than other display sets.
+- The hook uses memoization extensively to optimize performance and prevent unnecessary recalculations.
diff --git a/platform/docs/docs/platform/hooks/useViewportHover.md b/platform/docs/docs/platform/hooks/useViewportHover.md
new file mode 100644
index 000000000..61c45a1de
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useViewportHover.md
@@ -0,0 +1,57 @@
+---
+title: useViewportHover
+summary: A React hook that tracks mouse hover state and active status for a specific viewport.
+---
+
+# useViewportHover
+
+The `useViewportHover` hook provides information about whether the mouse is currently hovering over a specific viewport and whether that viewport is active.
+
+## Overview
+
+This hook monitors mouse movement to track hover state over a viewport element by its ID. It also checks whether the viewport is currently active (selected) in the viewer. This is useful for implementing conditional UI elements or behaviors that depend on user interaction with viewports.
+
+## Import
+
+```js
+import { useViewportHover } from '@ohif/extension-cornerstone';
+```
+
+## Usage
+
+```jsx
+function ViewportOverlay({ viewportId }) {
+ const { isHovered, isActive } = useViewportHover(viewportId);
+
+ return (
+
+ {isHovered && !isActive && (
+
Click to activate
+ )}
+ {isActive && (
+
Active viewport controls
+ )}
+
+ );
+}
+```
+
+## Parameters
+
+- `viewportId` (required): The ID of the viewport to track hover state for
+
+## Returns
+
+An object containing the following properties:
+
+- `isHovered`: Boolean indicating if the mouse is currently hovering over the viewport
+- `isActive`: Boolean indicating if the viewport is currently the active viewport in the grid
+
+## Implementation Details
+
+- The hook uses the DOM to find the viewport element by its `data-viewportId` attribute.
+- It calculates and maintains the viewport's bounding rectangle to efficiently determine if the mouse is within the viewport's bounds.
+- The viewport element's rectangle is updated when the window is resized.
+- Global mouse movement is tracked to determine hover state, rather than relying on traditional mouseenter/mouseleave events.
+- The hook automatically cleans up event listeners when the component unmounts or the viewport ID changes.
+- Active viewport state is derived from the viewport grid state using the `useViewportGrid` hook.
\ No newline at end of file
diff --git a/platform/docs/docs/platform/hooks/useViewportSegmentations.md b/platform/docs/docs/platform/hooks/useViewportSegmentations.md
new file mode 100644
index 000000000..26d59e931
--- /dev/null
+++ b/platform/docs/docs/platform/hooks/useViewportSegmentations.md
@@ -0,0 +1,95 @@
+---
+title: useViewportSegmentations
+summary: A React hook that provides segmentation data and representations for the active viewport with automatic updates when segmentations change.
+---
+
+# useViewportSegmentations
+
+The `useViewportSegmentations` hook provides access to segmentation data and their representations for a specific viewport, with automatic updates when segmentations are modified, removed, or representations change.
+
+## Overview
+
+This hook retrieves all segmentations and their representations for a given viewport from the segmentation service. It maps the segmentation data to a display-friendly format, including readable text for statistics. The hook monitors various segmentation and viewport events to update automatically when changes occur.
+
+## Import
+
+```js
+import { useViewportSegmentations } from '@ohif/extension-cornerstone';
+```
+
+## Usage
+
+```jsx
+function SegmentationPanel({ viewportId }) {
+ const { segmentationsWithRepresentations, disabled } = useViewportSegmentations({
+ viewportId,
+ subscribeToDataModified: true,
+ debounceTime: 100,
+ });
+
+ if (disabled) {
+ return Segmentations not available for this modality
;
+ }
+
+ if (!segmentationsWithRepresentations.length) {
+ return No segmentations available
;
+ }
+
+ return (
+
+ {segmentationsWithRepresentations.map(({ segmentation, representation }) => (
+
+
{segmentation.label}
+ {Object.entries(segmentation.segments).map(([segmentIndex, segment]) => (
+
+
{segment.label}
+ {segment.displayText.primary.map((text, i) => (
+
{text}
+ ))}
+
+ ))}
+
+ ))}
+
+ );
+}
+```
+
+## Parameters
+
+- `options` - Configuration options:
+ - `viewportId` (required): The ID of the viewport to get segmentations for
+ - `subscribeToDataModified` (optional): Whether to subscribe to segmentation data modifications (default: false)
+ - `debounceTime` (optional): Debounce time in milliseconds for updates (default: 0)
+
+## Returns
+
+An object with the following properties:
+
+- `segmentationsWithRepresentations`: An array of objects with each containing:
+ - `representation`: The segmentation representation in the viewport
+ - `segmentation`: The mapped segmentation data with display-friendly properties:
+ - `label`: The segmentation label
+ - `segments`: Object mapping segment indices to segment data:
+ - `label`: Segment label
+ - `color`: Segment color
+ - `displayText`: Organized text for display with `primary` and `secondary` arrays
+
+- `disabled`: Boolean indicating if segmentations are disabled for the current modality
+
+## Events
+
+The hook automatically updates when any of these events occur:
+- `SEGMENTATION_MODIFIED`
+- `SEGMENTATION_REMOVED`
+- `SEGMENTATION_REPRESENTATION_MODIFIED`
+- `ACTIVE_VIEWPORT_ID_CHANGED`
+- `GRID_STATE_CHANGED`
+- `SEGMENTATION_DATA_MODIFIED` (only if `subscribeToDataModified` is true)
+
+## Implementation Details
+
+- The hook excludes certain modalities from segmentation display: 'SM', 'OT', 'DOC', 'ECG'.
+- It uses debouncing to prevent excessive re-renders when multiple segmentation events occur in rapid succession.
+- Segmentation statistics are automatically mapped to readable text using the customization service.
+- Nested statistics are displayed with indentation to maintain hierarchy.
diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
index 1bb0b3d10..3763f3ae8 100644
--- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
+++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
@@ -23,7 +23,6 @@ import segDisplayEditingTrue from '../../../assets/img/segDisplayEditingTrue.png
import segDisplayEditingFalse from '../../../assets/img/segDisplayEditingFalse.png';
import thumbnailMenuItemsImage from '../../../assets/img/thumbnailMenuItemsImage.png';
import studyMenuItemsImage from '../../../assets/img/studyMenuItemsImage.png';
-import windowLevelActionMenu from '../../../assets/img/windowLevelActionMenu.png';
import viewPortNotificationImage from '../../../assets/img/viewport-notification.png';
import captureViewportModal from '../../../assets/img/captureViewportModal.png';
import aboutModal from '../../../assets/img/aboutModal.png';
@@ -201,31 +200,6 @@ window.config = {
],
`,
},
- {
- id: 'viewportActionMenu.windowLevelActionMenu',
- description:
- 'Configures the display and location of the window level action menu in the viewport.',
- image: windowLevelActionMenu,
- default: null,
- configuration: `
- window.config = {
- // rest of window config
- customizationService: [
- {
- 'viewportActionMenu.windowLevelActionMenu': {
- $merge: {
- location: 0, // Set the location of the menu in the viewport.
- // 0: topLeft
- // 1: topRight
- // 2: bottomLeft
- // 3: bottomRight
- }
- },
- },
- ],
- };
- `,
- },
{
id: 'measurementLabels',
description: 'Labels for measurement tools in the viewer that are automatically asked for.',
@@ -936,56 +910,6 @@ window.config = {
};
`,
},
- {
- id: 'viewportActionMenu.windowLevelActionMenu',
- description:
- 'Configures the display and location of the window level action menu in the viewport.',
- image: windowLevelActionMenu,
- default: null,
- configuration: `
- window.config = {
- // rest of window config
- customizationService: [
- {
- 'viewportActionMenu.windowLevelActionMenu': {
- $merge: {
- location: 0, // Set the location of the menu in the viewport.
- // 0: topLeft
- // 1: topRight
- // 2: bottomLeft
- // 3: bottomRight
- }
- },
- },
- ],
- };
- `,
- },
- {
- id: 'viewportActionMenu.dataOverlayMenu',
- description: 'Configures the display and location of the data overlay in the viewport.',
- image: segmentationOverlay,
- default: null,
- configuration: `
- window.config = {
- // rest of window config
- customizationService: [
- {
- 'viewportActionMenu.dataOverlayMenu': {
- $merge: {
- enabled: true,
- location: 1, // Set the location of the overlay in the viewport.
- // 0: topLeft
- // 1: topRight
- // 2: bottomLeft
- // 3: bottomRight
- }
- },
- },
- ],
- };
- `,
- },
{
id: 'viewportNotification.beginTrackingMessage',
description: 'Define the content to be displayed in begin tracking prompt',
diff --git a/platform/ui-next/src/components/AllInOneMenu/Menu.tsx b/platform/ui-next/src/components/AllInOneMenu/Menu.tsx
index 0fe900e55..d769ddb5b 100644
--- a/platform/ui-next/src/components/AllInOneMenu/Menu.tsx
+++ b/platform/ui-next/src/components/AllInOneMenu/Menu.tsx
@@ -50,7 +50,9 @@ export interface MenuProps {
showHeaderDivider?: boolean;
activePanelIndex?: number;
onVisibilityChange?: (isVisible: boolean) => void;
- horizontalDirection?: HorizontalDirection;
+ // New props that can be used as alternatives to horizontalDirection and verticalDirection
+ align?: 'start' | 'end' | 'center';
+ side?: 'top' | 'bottom' | 'left' | 'right';
children: ReactNode;
}
type MenuContextProps = {
@@ -58,6 +60,7 @@ type MenuContextProps = {
hideMenu: () => void;
addItemPanel: (index: number, label: string) => void;
horizontalDirection: HorizontalDirection;
+ verticalDirection?: VerticalDirection;
activePanelIndex: number;
};
@@ -76,9 +79,24 @@ const Menu = (props: MenuProps) => {
preventHideMenu,
menuClassName,
menuStyle,
- horizontalDirection = HorizontalDirection.LeftToRight,
+ align,
+ side,
} = props;
+ // Derive horizontalDirection and verticalDirection from align and side if provided
+ let horizontalDirection = HorizontalDirection.LeftToRight;
+ let verticalDirection = VerticalDirection.BottomToTop;
+
+ if (align !== undefined) {
+ horizontalDirection =
+ align === 'start' ? HorizontalDirection.LeftToRight : HorizontalDirection.RightToLeft;
+ }
+
+ if (side !== undefined) {
+ verticalDirection =
+ side === 'bottom' ? VerticalDirection.TopToBottom : VerticalDirection.BottomToTop;
+ }
+
const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
// The menuPath is an array consisting of this top Menu and every SubMenu
@@ -151,6 +169,7 @@ const Menu = (props: MenuProps) => {
addItemPanel,
activePanelIndex: currentMenuActivePanelIndex,
horizontalDirection,
+ verticalDirection,
}}
>
{isMenuVisible && (
diff --git a/platform/ui-next/src/components/CinePlayer/CinePlayer.tsx b/platform/ui-next/src/components/CinePlayer/CinePlayer.tsx
index 74275c9ba..e10b21b95 100644
--- a/platform/ui-next/src/components/CinePlayer/CinePlayer.tsx
+++ b/platform/ui-next/src/components/CinePlayer/CinePlayer.tsx
@@ -70,20 +70,7 @@ const CinePlayer: React.FC = ({
);
return (
-
- {isDynamic && dynamicInfo && (
-
handleDimensionGroupNumberChange(val as number)}
- className="mb-3 w-full"
- >
-
-
- )}
+
+
+ {isDynamic && dynamicInfo && (
+
handleDimensionGroupNumberChange(val as number)}
+ className="mt-3 w-full"
+ >
+
+
+ )}
);
};
diff --git a/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx
index 50b9b3e1f..20a5a1484 100644
--- a/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx
+++ b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx
@@ -12,7 +12,6 @@ interface DoubleSliderProps {
defaultValue?: [number, number];
onValueChange?: (value: [number, number]) => void;
showNumberInputs?: boolean;
- lockMode?: boolean;
}
const DoubleSlider = React.forwardRef
(
@@ -25,19 +24,10 @@ const DoubleSlider = React.forwardRef(
step = 1,
defaultValue = [min, max],
showNumberInputs = false,
- lockMode = false,
},
ref
) => {
const [value, setValue] = React.useState<[number, number]>(defaultValue);
- const trackRef = React.useRef(null);
- const dragStateRef = React.useRef<{
- startX: number;
- startValue: [number, number];
- rangeWidth: number;
- trackLeft: number;
- trackWidth: number;
- } | null>(null);
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
@@ -60,172 +50,8 @@ const DoubleSlider = React.forwardRef(
return Math.round(num * inverse) / inverse;
};
- const handleRangePointerDown = (event: React.PointerEvent) => {
- if (!lockMode) {
- return;
- }
- event.preventDefault();
- const rect = trackRef.current!.getBoundingClientRect();
- dragStateRef.current = {
- startX: event.clientX,
- startValue: [...value] as [number, number],
- rangeWidth: value[1] - value[0],
- trackLeft: rect.left,
- trackWidth: rect.width,
- };
- window.addEventListener('pointermove', handleRangePointerMove);
- window.addEventListener('pointerup', handleRangePointerUp, { once: true });
- };
-
- const handleRangePointerMove = (event: PointerEvent) => {
- const state = dragStateRef.current;
- if (!state) {
- return;
- }
- const { startX, startValue, rangeWidth, trackWidth } = state;
- const dxPx = event.clientX - startX;
- const pct = dxPx / trackWidth;
- const delta = pct * (max - min);
-
- let newLeft = startValue[0] + delta;
- let newRight = newLeft + rangeWidth;
-
- // clamp to bounds
- if (newLeft < min) {
- newLeft = min;
- newRight = min + rangeWidth;
- } else if (newRight > max) {
- newRight = max;
- newLeft = max - rangeWidth;
- }
-
- const clampedLeft = roundToStep(newLeft);
- const clampedRight = roundToStep(newRight);
- setValue([clampedLeft, clampedRight]);
- onValueChange?.([clampedLeft, clampedRight]);
- };
-
- const handleRangePointerUp = () => {
- dragStateRef.current = null;
- window.removeEventListener('pointermove', handleRangePointerMove);
- };
-
const handleSliderChange = React.useCallback(
(newValue: number[]) => {
- // Default behavior (no lock mode)
- if (!lockMode) {
- const clampedValue: [number, number] = [
- roundToStep(Math.max(min, Math.min(newValue[0], max))),
- roundToStep(Math.min(max, Math.max(newValue[1], min))),
- ];
- setValue(clampedValue);
- onValueChange?.(clampedValue);
- return;
- }
-
- // Lock mode behavior
- // First, determine if this is a symmetric expansion/contraction or a range shift
- // by checking which thumb(s) moved
- const isLeftThumbMoved = newValue[0] !== value[0];
- const isRightThumbMoved = newValue[1] !== value[1];
-
- // Case 1: Left thumb moved, right thumb needs to move symmetrically (opposite)
- if (isLeftThumbMoved && !isRightThumbMoved) {
- const delta = newValue[0] - value[0];
- const centerPoint = (value[0] + value[1]) / 2;
-
- // Symmetric movement: move right thumb in opposite direction
- const newRight = value[1] - delta;
-
- // Ensure values stay within bounds
- const clampedLeft = roundToStep(Math.max(min, Math.min(newValue[0], max)));
- const clampedRight = roundToStep(Math.min(max, Math.max(newRight, min)));
-
- // Special case: If right would go out of bounds, adjust left too to maintain center
- if (newRight !== clampedRight) {
- // Calculate new delta given the clamped right value
- const adjustedDelta = value[1] - clampedRight;
- // Apply same delta to left but in opposite direction
- const adjustedLeft = value[0] + adjustedDelta;
- const finalClampedLeft = roundToStep(Math.max(min, Math.min(adjustedLeft, max)));
-
- const finalValues: [number, number] = [finalClampedLeft, clampedRight];
- setValue(finalValues);
- onValueChange?.(finalValues);
- return;
- }
-
- const finalValues: [number, number] = [clampedLeft, clampedRight];
- setValue(finalValues);
- onValueChange?.(finalValues);
- return;
- }
-
- // Case 2: Right thumb moved, left thumb needs to move symmetrically (opposite)
- if (!isLeftThumbMoved && isRightThumbMoved) {
- const delta = newValue[1] - value[1];
- const centerPoint = (value[0] + value[1]) / 2;
-
- // Symmetric movement: move left thumb in opposite direction
- const newLeft = value[0] - delta;
-
- // Ensure values stay within bounds
- const clampedRight = roundToStep(Math.min(max, Math.max(newValue[1], min)));
- const clampedLeft = roundToStep(Math.max(min, Math.min(newLeft, max)));
-
- // Special case: If left would go out of bounds, adjust right too to maintain center
- if (newLeft !== clampedLeft) {
- // Calculate new delta given the clamped left value
- const adjustedDelta = value[0] - clampedLeft;
- // Apply same delta to right but in opposite direction
- const adjustedRight = value[1] + adjustedDelta;
- const finalClampedRight = roundToStep(Math.min(max, Math.max(adjustedRight, min)));
-
- const finalValues: [number, number] = [clampedLeft, finalClampedRight];
- setValue(finalValues);
- onValueChange?.(finalValues);
- return;
- }
-
- const finalValues: [number, number] = [clampedLeft, clampedRight];
- setValue(finalValues);
- onValueChange?.(finalValues);
- return;
- }
-
- // Case 3: Both thumbs moved (range shift) - maintain the same distance between thumbs
- if (isLeftThumbMoved && isRightThumbMoved) {
- const rangeWidth = value[1] - value[0];
-
- // Calculate how much the left thumb moved
- const leftDelta = newValue[0] - value[0];
-
- // New values maintaining the same range width
- let newLeft = newValue[0];
- let newRight = newLeft + rangeWidth;
-
- // Handle bounds checking
- if (newRight > max) {
- newRight = max;
- newLeft = newRight - rangeWidth;
- }
-
- if (newLeft < min) {
- newLeft = min;
- newRight = newLeft + rangeWidth;
- }
-
- const clampedValues: [number, number] = [
- roundToStep(Math.max(min, Math.min(newLeft, max))),
- roundToStep(Math.min(max, Math.max(newRight, min))),
- ];
-
- setValue(clampedValues);
- onValueChange?.(clampedValues);
- return;
- }
-
- // Fallback to default behavior
const clampedValue: [number, number] = [
roundToStep(Math.max(min, Math.min(newValue[0], max))),
roundToStep(Math.min(max, Math.max(newValue[1], min))),
@@ -233,86 +59,25 @@ const DoubleSlider = React.forwardRef(
setValue(clampedValue);
onValueChange?.(clampedValue);
},
- [min, max, onValueChange, step, value, lockMode]
+ [min, max, onValueChange, step]
);
const handleInputChange = React.useCallback(
(index: 0 | 1, inputValue: string) => {
const newValue = parseFloat(inputValue);
if (!isNaN(newValue)) {
- // Default behavior (no lock mode)
- if (!lockMode) {
- const clampedValue: [number, number] = [...value];
- clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
- if (index === 0 && clampedValue[0] > clampedValue[1]) {
- clampedValue[1] = clampedValue[0];
- } else if (index === 1 && clampedValue[1] < clampedValue[0]) {
- clampedValue[0] = clampedValue[1];
- }
- setValue(clampedValue);
- onValueChange?.(clampedValue);
- return;
- }
-
- // Lock mode behavior
- const centerPoint = (value[0] + value[1]) / 2;
- const rangeWidth = value[1] - value[0];
const clampedValue: [number, number] = [...value];
-
- // Calculate new value with constraints
- const boundedNewValue = roundToStep(Math.min(Math.max(newValue, min), max));
-
- if (index === 0) {
- // Left thumb changed
- const delta = boundedNewValue - value[0];
-
- // Symmetric change: move right thumb in opposite direction
- const newRight = value[1] - delta;
- const boundedNewRight = roundToStep(Math.min(Math.max(newRight, min), max));
-
- // If right would go out of bounds, adjust left too
- if (newRight !== boundedNewRight) {
- // Adjust to maintain the same width but respect bounds
- if (boundedNewRight === max) {
- clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min));
- clampedValue[1] = max;
- } else if (boundedNewRight === min) {
- clampedValue[0] = min;
- clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max));
- }
- } else {
- clampedValue[0] = boundedNewValue;
- clampedValue[1] = boundedNewRight;
- }
- } else {
- // Right thumb changed
- const delta = boundedNewValue - value[1];
-
- // Symmetric change: move left thumb in opposite direction
- const newLeft = value[0] - delta;
- const boundedNewLeft = roundToStep(Math.min(Math.max(newLeft, min), max));
-
- // If left would go out of bounds, adjust right too
- if (newLeft !== boundedNewLeft) {
- // Adjust to maintain the same width but respect bounds
- if (boundedNewLeft === min) {
- clampedValue[0] = min;
- clampedValue[1] = roundToStep(Math.min(min + rangeWidth, max));
- } else if (boundedNewLeft === max) {
- clampedValue[0] = roundToStep(Math.max(max - rangeWidth, min));
- clampedValue[1] = max;
- }
- } else {
- clampedValue[0] = boundedNewLeft;
- clampedValue[1] = boundedNewValue;
- }
+ clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
+ if (index === 0 && clampedValue[0] > clampedValue[1]) {
+ clampedValue[1] = clampedValue[0];
+ } else if (index === 1 && clampedValue[1] < clampedValue[0]) {
+ clampedValue[0] = clampedValue[1];
}
-
setValue(clampedValue);
onValueChange?.(clampedValue);
}
},
- [value, min, max, onValueChange, step, lockMode]
+ [value, min, max, onValueChange, step]
);
const formatValue = (val: number) => {
@@ -344,14 +109,8 @@ const DoubleSlider = React.forwardRef(
value={value}
onValueChange={handleSliderChange}
>
-
-
+
+
diff --git a/platform/ui-next/src/components/Header/Header.tsx b/platform/ui-next/src/components/Header/Header.tsx
index 97b3ffcec..480b45b28 100644
--- a/platform/ui-next/src/components/Header/Header.tsx
+++ b/platform/ui-next/src/components/Header/Header.tsx
@@ -1,5 +1,4 @@
import React, { ReactNode } from 'react';
-import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import {
DropdownMenu,
@@ -8,7 +7,9 @@ import {
DropdownMenuItem,
Icons,
Button,
+ ToolButton,
} from '../';
+import { IconPresentationProvider } from '@ohif/ui-next';
import NavBar from '../NavBar';
@@ -51,72 +52,77 @@ function Header({
};
return (
-
-
-
-
- {isReturnEnabled &&
}
-
- {WhiteLabeling?.createLogoComponentFn?.(React, props) ||
}
+
+
+
+
+ {isReturnEnabled &&
}
+
+ {WhiteLabeling?.createLogoComponentFn?.(React, props) || }
+
+
+
+
{Secondary}
+
+
+ {UndoRedo}
+
+ {PatientInfo}
+
+
+
+
+
+
+
+
+
+ {menuOptions.map((option, index) => {
+ const IconComponent = option.icon
+ ? Icons[option.icon as keyof typeof Icons]
+ : null;
+ return (
+
+ {IconComponent && (
+
+
+
+ )}
+ {option.title}
+
+ );
+ })}
+
+
- {Secondary}
-
-
- {UndoRedo}
-
- {PatientInfo}
-
-
-
-
-
-
-
-
-
- {menuOptions.map((option, index) => {
- const IconComponent = option.icon
- ? Icons[option.icon as keyof typeof Icons]
- : null;
- return (
-
- {IconComponent && (
-
-
-
- )}
- {option.title}
-
- );
- })}
-
-
-
-
-
-
+
+
);
}
diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx
index 953161288..f3daa6443 100644
--- a/platform/ui-next/src/components/Icons/Icons.tsx
+++ b/platform/ui-next/src/components/Icons/Icons.tsx
@@ -79,6 +79,9 @@ import OrientationSwitch from './Sources/OrientationSwitch';
import LayerBackground from './Sources/LayerBackground';
import LayerForeground from './Sources/LayerForeground';
import LayerSegmentation from './Sources/LayerSegmentation';
+import WindowLevelAdvanced from './Sources/WindowLevelAdvanced';
+import Opacity from './Sources/Opacity';
+import Threshold from './Sources/Threshold';
import {
Tool3DRotate,
ToolAngle,
@@ -583,6 +586,9 @@ export const Icons = {
OHIFLogoColorDarkBackground,
Magnifier,
Pencil,
+ WindowLevelAdvanced,
+ Opacity,
+ Threshold,
//
//
//
diff --git a/platform/ui-next/src/components/Icons/Sources/Close.tsx b/platform/ui-next/src/components/Icons/Sources/Close.tsx
index 308317d4d..a96308558 100644
--- a/platform/ui-next/src/components/Icons/Sources/Close.tsx
+++ b/platform/ui-next/src/components/Icons/Sources/Close.tsx
@@ -3,16 +3,24 @@ import type { IconProps } from '../types';
export const Close = (props: IconProps) => (
+
);
diff --git a/platform/ui-next/src/components/Icons/Sources/Opacity.tsx b/platform/ui-next/src/components/Icons/Sources/Opacity.tsx
new file mode 100644
index 000000000..ea1902a2c
--- /dev/null
+++ b/platform/ui-next/src/components/Icons/Sources/Opacity.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import type { IconProps } from '../types';
+
+export const Opacity = (props: IconProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default Opacity;
diff --git a/platform/ui-next/src/components/Icons/Sources/Threshold.tsx b/platform/ui-next/src/components/Icons/Sources/Threshold.tsx
new file mode 100644
index 000000000..d3bd401aa
--- /dev/null
+++ b/platform/ui-next/src/components/Icons/Sources/Threshold.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import type { IconProps } from '../types';
+
+export const Threshold = (props: IconProps) => (
+
+
+
+
+
+
+);
+
+export default Threshold;
diff --git a/platform/ui-next/src/components/Icons/Sources/WindowLevelAdvanced.tsx b/platform/ui-next/src/components/Icons/Sources/WindowLevelAdvanced.tsx
new file mode 100644
index 000000000..e4759b1ff
--- /dev/null
+++ b/platform/ui-next/src/components/Icons/Sources/WindowLevelAdvanced.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import type { IconProps } from '../types';
+
+export const WindowLevelAdvanced = (props: IconProps) => (
+
+
+
+
+
+);
+
+export default WindowLevelAdvanced;
diff --git a/platform/ui-next/src/components/Numeric/Numeric.tsx b/platform/ui-next/src/components/Numeric/Numeric.tsx
index eca111654..40badb61f 100644
--- a/platform/ui-next/src/components/Numeric/Numeric.tsx
+++ b/platform/ui-next/src/components/Numeric/Numeric.tsx
@@ -29,7 +29,6 @@ interface NumericMetaContextValue {
min: number;
max: number;
step: number;
- lockMode?: boolean;
}
const NumericMetaContext = createContext
(null);
@@ -48,7 +47,6 @@ interface NumericMetaContainerProps {
max?: number;
step?: number;
className?: string;
- lockMode?: boolean; // Add lock mode prop for doubleRange
}
function NumericMetaContainer({
@@ -62,7 +60,6 @@ function NumericMetaContainer({
max = 100,
step = 1,
className,
- lockMode = false,
children,
}: PropsWithChildren) {
// Calculate default values based on min and max
@@ -118,7 +115,6 @@ function NumericMetaContainer({
min,
max,
step,
- lockMode,
}}
>
{children}
@@ -240,7 +236,7 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
throw new Error('DoubleRange must be used inside .');
}
- const { mode, doubleValue, setDoubleValue, min, max, step, lockMode } = ctx;
+ const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
const handleSliderChange = useCallback(
(values: [number, number]) => {
@@ -262,7 +258,6 @@ function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
defaultValue={doubleValue}
onValueChange={handleSliderChange}
showNumberInputs={showNumberInputs}
- lockMode={lockMode}
/>
);
diff --git a/platform/ui-next/src/components/Select/Select.tsx b/platform/ui-next/src/components/Select/Select.tsx
index 441a3abce..116a14438 100644
--- a/platform/ui-next/src/components/Select/Select.tsx
+++ b/platform/ui-next/src/components/Select/Select.tsx
@@ -119,7 +119,13 @@ const SelectItem = React.forwardRef<
-
{children}
+ {typeof children === 'string' ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
diff --git a/platform/ui-next/src/components/ToolButton/ToolButton.tsx b/platform/ui-next/src/components/ToolButton/ToolButton.tsx
index 6e93779da..fa221c9de 100644
--- a/platform/ui-next/src/components/ToolButton/ToolButton.tsx
+++ b/platform/ui-next/src/components/ToolButton/ToolButton.tsx
@@ -3,6 +3,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
import { Icons } from '../Icons';
import { Button } from '../Button';
import { cn } from '../../lib/utils';
+import { useIconPresentation } from '../../contextProviders/IconPresentationProvider';
const baseClasses = '!rounded-lg inline-flex items-center justify-center';
const defaultClasses = 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight';
@@ -19,6 +20,10 @@ const sizeClasses = {
buttonSizeClass: 'w-8 h-8',
iconSizeClass: 'h-6 w-6',
},
+ tiny: {
+ buttonSizeClass: 'w-6 h-6',
+ iconSizeClass: 'h-4 w-4',
+ },
};
interface ToolButtonProps {
@@ -33,6 +38,7 @@ interface ToolButtonProps {
commands?: Record
;
onInteraction?: (details: { itemId: string; commands?: Record }) => void;
className?: string;
+ children?: React.ReactNode;
}
function ToolButton(props: ToolButtonProps) {
@@ -48,8 +54,10 @@ function ToolButton(props: ToolButtonProps) {
commands,
onInteraction,
className,
+ children,
} = props;
+ const { className: iconClassName } = useIconPresentation();
const { buttonSizeClass, iconSizeClass } = sizeClasses[size] || sizeClasses.default;
const buttonClasses = cn(
@@ -90,10 +98,12 @@ function ToolButton(props: ToolButtonProps) {
aria-label={defaultTooltip}
disabled={disabled}
>
-
+ {children || (
+
+ )}
diff --git a/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx b/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx
index e2374a470..062807c40 100644
--- a/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx
+++ b/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx
@@ -141,7 +141,6 @@ const ToolButtonListItem = React.forwardRef<
ToolButtonListItemProps
>(({ className, children, icon, disabledText, tooltip, disabled, ...props }, ref) => {
const defaultTooltip = tooltip || (typeof children === 'string' ? children : undefined);
- const hasTooltip = defaultTooltip || disabledText;
const menuItem = (
{children};
}
+function TopMiddle({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function BottomMiddle({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function LeftMiddle({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
+function RightMiddle({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+
export const ViewportActionCorners = {
Container,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
+ TopMiddle,
+ BottomMiddle,
+ LeftMiddle,
+ RightMiddle,
};
diff --git a/platform/ui-next/src/components/Viewport/ViewportOverlay.css b/platform/ui-next/src/components/Viewport/ViewportOverlay.css
index 5b17f7ad8..40a59a7b4 100644
--- a/platform/ui-next/src/components/Viewport/ViewportOverlay.css
+++ b/platform/ui-next/src/components/Viewport/ViewportOverlay.css
@@ -14,7 +14,7 @@
top: 2.15rem;
}
.overlay-bottom {
- bottom: 1rem;
+ bottom: 2.15rem;
}
.overlay-text {
diff --git a/platform/ui-next/src/contextProviders/IconPresentationProvider.tsx b/platform/ui-next/src/contextProviders/IconPresentationProvider.tsx
new file mode 100644
index 000000000..d951a98ab
--- /dev/null
+++ b/platform/ui-next/src/contextProviders/IconPresentationProvider.tsx
@@ -0,0 +1,88 @@
+import React, { createContext, useContext, ReactNode, ComponentType } from 'react';
+import { cn } from '../utils';
+import { Button } from '../components/Button';
+
+export type IconSizeType = 'tiny' | 'small' | 'medium' | 'large' | number;
+
+interface IconSizeContextType {
+ size: IconSizeType;
+ getSizeValue: (size?: IconSizeType) => number | string;
+ getSizeClassName: (size?: IconSizeType, additionalClasses?: string) => string;
+ IconContainer: ComponentType;
+ containerProps: {
+ variant: string;
+ size: string;
+ [key: string]: any;
+ };
+ className: string;
+}
+
+const sizeMap = {
+ tiny: 16,
+ small: 20,
+ medium: 24,
+ large: 28,
+};
+
+export const getSizeValue = (size: IconSizeType = 'medium'): number | string => {
+ if (typeof size === 'number') {
+ return size;
+ }
+ return sizeMap[size] || sizeMap.medium;
+};
+
+export const getSizeClassName = (size: IconSizeType = 'medium', additionalClasses = ''): string => {
+ const sizeValue = getSizeValue(size);
+ return cn(`h-[${sizeValue}px] w-[${sizeValue}px]`, additionalClasses);
+};
+
+const defaultContext: IconSizeContextType = {
+ size: 'medium',
+ getSizeValue,
+ getSizeClassName,
+ IconContainer: Button,
+ containerProps: {
+ variant: 'ghost',
+ size: 'icon',
+ },
+ className: '',
+};
+
+export const IconSizeContext = createContext(defaultContext);
+
+interface IconPresentationProviderProps {
+ size: IconSizeType;
+ children: ReactNode;
+ IconContainer?: ComponentType;
+ containerProps?: {
+ variant?: string;
+ size?: string;
+ [key: string]: any;
+ };
+}
+
+export const IconPresentationProvider = ({
+ size,
+ children,
+ IconContainer = Button,
+ containerProps = {},
+}: IconPresentationProviderProps) => {
+ const className = getSizeClassName(size);
+ const mergedProps = {
+ variant: 'ghost',
+ size: 'icon',
+ ...containerProps,
+ };
+
+ const contextValue = {
+ size,
+ getSizeValue,
+ getSizeClassName,
+ IconContainer,
+ containerProps: mergedProps,
+ className,
+ };
+ return {children} ;
+};
+
+export const useIconPresentation = () => useContext(IconSizeContext);
diff --git a/platform/ui-next/src/contextProviders/ViewportActionCornersProvider.tsx b/platform/ui-next/src/contextProviders/ViewportActionCornersProvider.tsx
deleted file mode 100644
index 55fe7dc2d..000000000
--- a/platform/ui-next/src/contextProviders/ViewportActionCornersProvider.tsx
+++ /dev/null
@@ -1,550 +0,0 @@
-import React, {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useReducer,
- ReactNode,
- useMemo,
-} from 'react';
-import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
-import { ActionComponentInfo, AlignAndSide } from '../types';
-
-const DEFAULT_STATE = {
- viewports: {} as Record>,
-};
-
-/**
- * API for managing viewport action corner items.
- * These methods control the state and behavior of the corner items themselves,
- * not the content they might affect in the viewport.
- */
-interface ViewportActionCornersApi {
- getState: () => typeof DEFAULT_STATE;
- addComponent: (component: ActionComponentInfo) => void;
- addComponents: (components: Array) => void;
- clear: (viewportId: string) => void;
- getAlignAndSide: (location: ViewportActionCornersLocations) => AlignAndSide;
- lockItem: (viewportId: string, itemId: string) => void;
- unlockItem: (viewportId: string, itemId: string) => void;
- toggleLock: (viewportId: string, itemId: string) => void;
- isItemLocked: (viewportId: string, itemId: string) => boolean;
- showItem: (viewportId: string, itemId: string) => void;
- hideItem: (viewportId: string, itemId: string) => void;
- toggleVisibility: (viewportId: string, itemId: string) => void;
- isItemVisible: (viewportId: string, itemId: string) => boolean;
- openItem: (viewportId: string, itemId: string) => void;
- closeItem: (viewportId: string, itemId: string) => void;
- closeAllItems: (viewportId: string) => void;
- isItemOpen: (viewportId: string, itemId: string) => boolean;
-}
-
-export const ViewportActionCornersContext = createContext<
- [typeof DEFAULT_STATE, ViewportActionCornersApi]
->([DEFAULT_STATE, {} as ViewportActionCornersApi]);
-
-interface ViewportActionCornersProviderProps {
- children: ReactNode;
- service: any;
-}
-
-export function ViewportActionCornersProvider({
- children,
- service,
-}: ViewportActionCornersProviderProps) {
- const viewportActionCornersReducer = (state = DEFAULT_STATE, action) => {
- switch (action.type) {
- case 'ADD_COMPONENT': {
- const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
-
- const newState = { ...state };
-
- if (!newState.viewports[viewportId]) {
- newState.viewports[viewportId] = {
- [ViewportActionCornersLocations.topLeft]: [],
- [ViewportActionCornersLocations.topRight]: [],
- [ViewportActionCornersLocations.bottomLeft]: [],
- [ViewportActionCornersLocations.bottomRight]: [],
- };
- }
-
- if (!newState.viewports[viewportId][location]) {
- newState.viewports[viewportId][location] = [];
- }
-
- const componentInfo = {
- id,
- component,
- indexPriority,
- isOpen: false,
- isVisible: true,
- isLocked: false,
- };
-
- const components = [...newState.viewports[viewportId][location]];
- const index = components.findIndex(item => item.indexPriority > indexPriority);
-
- if (index === -1) {
- components.push(componentInfo);
- } else {
- components.splice(index, 0, componentInfo);
- }
-
- newState.viewports[viewportId][location] = components;
-
- return newState;
- }
-
- case 'ADD_COMPONENTS': {
- const components = action.payload;
- let newState = { ...state };
-
- components.forEach(component => {
- newState = viewportActionCornersReducer(newState, {
- type: 'ADD_COMPONENT',
- payload: component,
- });
- });
-
- return newState;
- }
-
- case 'CLEAR': {
- const viewportId = action.payload;
- const newState = { ...state };
-
- if (newState.viewports[viewportId]) {
- const newViewports = { ...newState.viewports };
- delete newViewports[viewportId];
- newState.viewports = newViewports;
- }
-
- return newState;
- }
-
- case 'SET_LOCKED': {
- const { viewportId, itemId, lockedStatus } = action.payload;
- const newState = { ...state };
-
- if (!newState.viewports[viewportId]) {
- return state;
- }
-
- const viewportCopy = { ...newState.viewports[viewportId] };
-
- Object.keys(viewportCopy).forEach(locationKey => {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = [...viewportCopy[location]];
-
- const itemIndex = components.findIndex(item => item.id === itemId);
- if (itemIndex !== -1) {
- const updatedItem = { ...components[itemIndex], isLocked: lockedStatus };
- components[itemIndex] = updatedItem;
- viewportCopy[location] = components;
- }
- });
-
- newState.viewports[viewportId] = viewportCopy;
- return newState;
- }
-
- case 'SET_VISIBLE': {
- const { viewportId, itemId, visibleStatus } = action.payload;
- const newState = { ...state };
-
- if (!newState.viewports[viewportId]) {
- return state;
- }
-
- const viewportCopy = { ...newState.viewports[viewportId] };
-
- Object.keys(viewportCopy).forEach(locationKey => {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = [...viewportCopy[location]];
-
- const itemIndex = components.findIndex(item => item.id === itemId);
- if (itemIndex !== -1) {
- const updatedItem = { ...components[itemIndex], isVisible: visibleStatus };
- components[itemIndex] = updatedItem;
- viewportCopy[location] = components;
- }
- });
-
- newState.viewports[viewportId] = viewportCopy;
- return newState;
- }
-
- case 'OPEN_ITEM': {
- const { viewportId, itemId } = action.payload;
- const newState = { ...state };
-
- if (!newState.viewports[viewportId]) {
- return state;
- }
-
- const viewportCopy = { ...newState.viewports[viewportId] };
-
- // Update isOpen flag for the component with matching id in any location
- Object.keys(viewportCopy).forEach(locationKey => {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = [...viewportCopy[location]];
-
- const itemIndex = components.findIndex(item => item.id === itemId);
- if (itemIndex !== -1) {
- const updatedItem = { ...components[itemIndex], isOpen: true };
- components[itemIndex] = updatedItem;
- viewportCopy[location] = components;
- }
- });
-
- newState.viewports[viewportId] = viewportCopy;
- return newState;
- }
-
- case 'CLOSE_ITEM': {
- const { viewportId, itemId } = action.payload;
- const newState = { ...state };
-
- if (!newState.viewports[viewportId]) {
- return state;
- }
-
- const viewportCopy = { ...newState.viewports[viewportId] };
-
- // Update isOpen flag for the component with matching id in any location
- Object.keys(viewportCopy).forEach(locationKey => {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = [...viewportCopy[location]];
-
- const itemIndex = components.findIndex(item => item.id === itemId);
- if (itemIndex !== -1) {
- const updatedItem = { ...components[itemIndex], isOpen: false };
- components[itemIndex] = updatedItem;
- viewportCopy[location] = components;
- }
- });
-
- newState.viewports[viewportId] = viewportCopy;
- return newState;
- }
-
- case 'CLOSE_ALL_ITEMS': {
- const viewportId = action.payload;
- const newState = { ...state };
-
- if (!newState.viewports[viewportId]) {
- return state;
- }
-
- const viewportCopy = { ...newState.viewports[viewportId] };
-
- // Set isOpen to false for all components in the viewport
- Object.keys(viewportCopy).forEach(locationKey => {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = viewportCopy[location].map(item => ({
- ...item,
- isOpen: false,
- }));
-
- viewportCopy[location] = components;
- });
-
- newState.viewports[viewportId] = viewportCopy;
- return newState;
- }
-
- default:
- return state;
- }
- };
-
- const [state, dispatch] = useReducer(viewportActionCornersReducer, DEFAULT_STATE);
-
- const getState = useCallback(() => {
- return state;
- }, [state]);
-
- const addComponent = useCallback(
- (component: ActionComponentInfo) => {
- dispatch({
- type: 'ADD_COMPONENT',
- payload: component,
- });
- },
- [dispatch]
- );
-
- const addComponents = useCallback(
- (components: Array) => {
- dispatch({
- type: 'ADD_COMPONENTS',
- payload: components,
- });
- },
- [dispatch]
- );
-
- const clear = useCallback(
- (viewportId: string) => {
- dispatch({
- type: 'CLEAR',
- payload: viewportId,
- });
- },
- [dispatch]
- );
-
- const lockItem = useCallback(
- (viewportId: string, itemId: string) => {
- dispatch({
- type: 'SET_LOCKED',
- payload: { viewportId, itemId, lockedStatus: true },
- });
- },
- [dispatch]
- );
-
- const unlockItem = useCallback(
- (viewportId: string, itemId: string) => {
- dispatch({
- type: 'SET_LOCKED',
- payload: { viewportId, itemId, lockedStatus: false },
- });
- },
- [dispatch]
- );
-
- const toggleLock = useCallback(
- (viewportId: string, itemId: string) => {
- const currentLocked = isItemLocked(viewportId, itemId);
- dispatch({
- type: 'SET_LOCKED',
- payload: { viewportId, itemId, lockedStatus: !currentLocked },
- });
- },
- [dispatch]
- );
-
- const isItemLocked = useCallback(
- (viewportId: string, itemId: string) => {
- if (!state.viewports[viewportId]) {
- return false; // Default to unlocked if viewport doesn't exist
- }
-
- for (const locationKey in state.viewports[viewportId]) {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = state.viewports[viewportId][location];
- const item = components.find(item => item.id === itemId);
-
- if (item && item.isLocked === true) {
- return true;
- }
- }
-
- return false; // Default to unlocked if item not found or isLocked is undefined
- },
- [state.viewports]
- );
-
- const showItem = useCallback(
- (viewportId: string, itemId: string) => {
- dispatch({
- type: 'SET_VISIBLE',
- payload: { viewportId, itemId, visibleStatus: true },
- });
- },
- [dispatch]
- );
-
- const hideItem = useCallback(
- (viewportId: string, itemId: string) => {
- dispatch({
- type: 'SET_VISIBLE',
- payload: { viewportId, itemId, visibleStatus: false },
- });
- },
- [dispatch]
- );
-
- const toggleVisibility = useCallback(
- (viewportId: string, itemId: string) => {
- const currentVisible = isItemVisible(viewportId, itemId);
- dispatch({
- type: 'SET_VISIBLE',
- payload: { viewportId, itemId, visibleStatus: !currentVisible },
- });
- },
- [dispatch]
- );
-
- const isItemVisible = useCallback(
- (viewportId: string, itemId: string) => {
- if (!state.viewports[viewportId]) {
- return true; // Default to visible if viewport doesn't exist
- }
-
- for (const locationKey in state.viewports[viewportId]) {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = state.viewports[viewportId][location];
- const item = components.find(item => item.id === itemId);
-
- if (item && item.isVisible === false) {
- return false;
- }
- }
-
- return true; // Default to visible if item not found or isVisible is undefined
- },
- [state.viewports]
- );
-
- const openItem = useCallback(
- (viewportId: string, itemId: string) => {
- dispatch({
- type: 'OPEN_ITEM',
- payload: { viewportId, itemId },
- });
- },
- [dispatch]
- );
-
- const closeItem = useCallback(
- (viewportId: string, itemId: string) => {
- dispatch({
- type: 'CLOSE_ITEM',
- payload: { viewportId, itemId },
- });
- },
- [dispatch]
- );
-
- const closeAllItems = useCallback(
- (viewportId: string) => {
- dispatch({
- type: 'CLOSE_ALL_ITEMS',
- payload: viewportId,
- });
- },
- [dispatch]
- );
-
- const isItemOpen = useCallback(
- (viewportId: string, itemId: string) => {
- if (!state.viewports[viewportId]) {
- return false;
- }
-
- for (const locationKey in state.viewports[viewportId]) {
- const location = Number(locationKey) as ViewportActionCornersLocations;
- const components = state.viewports[viewportId][location];
- const item = components.find(item => item.id === itemId);
-
- if (item && item.isOpen === true) {
- return true;
- }
- }
-
- return false;
- },
- [state.viewports]
- );
-
- const getAlignAndSide = useCallback(
- (location: ViewportActionCornersLocations) => {
- return service.getAlignAndSide(location);
- },
- [service]
- );
-
- const api = useMemo(() => {
- return {
- getState,
- addComponent,
- addComponents,
- clear,
- getAlignAndSide,
- lockItem,
- unlockItem,
- toggleLock,
- isItemLocked,
- showItem,
- hideItem,
- toggleVisibility,
- isItemVisible,
- openItem,
- closeItem,
- closeAllItems,
- isItemOpen,
- };
- }, [
- getState,
- addComponent,
- addComponents,
- clear,
- getAlignAndSide,
- lockItem,
- unlockItem,
- toggleLock,
- isItemLocked,
- showItem,
- hideItem,
- toggleVisibility,
- isItemVisible,
- openItem,
- closeItem,
- closeAllItems,
- isItemOpen,
- ]);
-
- useEffect(() => {
- if (service && service.setServiceImplementation) {
- const implementation = {
- getState,
- addComponent,
- addComponents,
- clear,
- lockItem,
- unlockItem,
- toggleLock,
- isItemLocked,
- showItem,
- hideItem,
- toggleVisibility,
- isItemVisible,
- openItem,
- closeItem,
- closeAllItems,
- isItemOpen,
- };
-
- service.setServiceImplementation(implementation);
- }
- }, [
- service,
- getState,
- addComponent,
- addComponents,
- clear,
- lockItem,
- unlockItem,
- toggleLock,
- isItemLocked,
- showItem,
- hideItem,
- toggleVisibility,
- isItemVisible,
- openItem,
- closeItem,
- closeAllItems,
- isItemOpen,
- ]);
-
- return (
-
- {children}
-
- );
-}
-
-// Custom hook to use the ViewportActionCorners context
-export const useViewportActionCorners = () => useContext(ViewportActionCornersContext);
diff --git a/platform/ui-next/src/contextProviders/index.ts b/platform/ui-next/src/contextProviders/index.ts
index 72a4726c3..c37857c11 100644
--- a/platform/ui-next/src/contextProviders/index.ts
+++ b/platform/ui-next/src/contextProviders/index.ts
@@ -9,10 +9,6 @@ import { UserAuthenticationProvider, useUserAuthentication } from './UserAuthent
import { ImageViewerContext, ImageViewerProvider, useImageViewer } from './ImageViewerProvider';
import DragAndDropProvider from './DragAndDropProvider';
import CineProvider, { useCine } from './CineProvider';
-import {
- ViewportActionCornersProvider,
- useViewportActionCorners,
-} from './ViewportActionCornersProvider';
export { useNotification, NotificationProvider };
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
@@ -24,4 +20,4 @@ export { UserAuthenticationProvider, useUserAuthentication };
export { ImageViewerContext, ImageViewerProvider, useImageViewer };
export { DragAndDropProvider };
export { CineProvider, useCine };
-export { ViewportActionCornersProvider, useViewportActionCorners };
+export { IconPresentationProvider, useIconPresentation } from './IconPresentationProvider';
diff --git a/tests/SRHydration.spec.ts b/tests/SRHydration.spec.ts
index 7af167141..e9640b020 100644
--- a/tests/SRHydration.spec.ts
+++ b/tests/SRHydration.spec.ts
@@ -11,6 +11,7 @@ test('should hydrate SR reports correctly', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('trackedMeasurements-btn').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
+ await page.waitForTimeout(2000);
await checkForScreenshot(page, page, screenShotPaths.srHydration.srPreHydration);
await page.evaluate(() => {
@@ -33,6 +34,7 @@ test('should hydrate SR reports correctly', async ({ page }) => {
});
await page.getByTestId('yes-hydrate-btn').click();
+ await page.waitForTimeout(2000);
await checkForScreenshot(page, page, screenShotPaths.srHydration.srPostHydration);
await page.evaluate(() => {
@@ -55,5 +57,6 @@ test('should hydrate SR reports correctly', async ({ page }) => {
});
await page.getByTestId('data-row').first().click();
+ await page.waitForTimeout(2000);
await checkForScreenshot(page, page, screenShotPaths.srHydration.srJumpToMeasurement);
});
diff --git a/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png b/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png
index 436c4a89c..c88dcafbd 100644
Binary files a/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png and b/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png b/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png
index 889f98b5c..cd7065bb7 100644
Binary files a/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png and b/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png b/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png
index a929c1578..b9e0dd104 100644
Binary files a/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png and b/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png b/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png
index e8b2982d5..eb06c8247 100644
Binary files a/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png and b/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png b/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png
index 96b6d4ddb..660ada0bc 100644
Binary files a/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png and b/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png b/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png
index df405e246..f0cf26c25 100644
Binary files a/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png and b/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png b/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png
index 6802891fe..b057780e1 100644
Binary files a/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png and b/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png b/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png
index 9b88e8652..24690ef1a 100644
Binary files a/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png and b/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png b/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png
index 1bb545f78..a1db97291 100644
Binary files a/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png and b/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsNewDisplayset.png b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsNewDisplayset.png
index 96a504bf7..4f1ae25dd 100644
Binary files a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsNewDisplayset.png and b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsNewDisplayset.png differ
diff --git a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRendered.png b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRendered.png
index 7eaed9d41..6d6fa1cfb 100644
Binary files a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRendered.png and b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRendered.png differ
diff --git a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsResetToolbar.png b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsResetToolbar.png
index f8a11a732..be8db29dd 100644
Binary files a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsResetToolbar.png and b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsResetToolbar.png differ
diff --git a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRotated.png b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRotated.png
index 35d82c260..3de238e6c 100644
Binary files a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRotated.png and b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsRotated.png differ
diff --git a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsSlabThickness.png b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsSlabThickness.png
index 8ce32b624..5f52c3b58 100644
Binary files a/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsSlabThickness.png and b/tests/screenshots/chromium/Crosshairs.spec.ts/crosshairsSlabThickness.png differ
diff --git a/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png b/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png
index deac8ea94..ff9c7c1d7 100644
Binary files a/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png and b/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png b/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png
index f839c8f38..b8ec2cf34 100644
Binary files a/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png and b/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png b/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png
index 26950de59..b4378e3da 100644
Binary files a/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png and b/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png b/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png
index 182ce9953..922907130 100644
Binary files a/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png and b/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png
index 88c4f1ee3..102798dc7 100644
Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png differ
diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png
index 3ddd4da6e..124feade6 100644
Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png differ
diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png
index 4300b0283..d0086408b 100644
Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png differ
diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png
index 4300b0283..d0086408b 100644
Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png differ
diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png
index 16b934b20..c43b7e7d7 100644
Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png differ
diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png
index e1289ab21..420a426dd 100644
Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png differ
diff --git a/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png b/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png
index 9782c1c3b..2ea715d0a 100644
Binary files a/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png and b/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png b/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png
index dd64ddc29..f3d1e5350 100644
Binary files a/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png and b/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png b/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png
index c51740b2d..0129003de 100644
Binary files a/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png and b/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png b/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png
index 3d0fa3d47..658f6b240 100644
Binary files a/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png and b/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png b/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png
index 9b90f9448..23c5dd9be 100644
Binary files a/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png and b/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png differ
diff --git a/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png b/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png
index de155a567..fb3f0e173 100644
Binary files a/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png and b/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png differ
diff --git a/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png b/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png
index 0cb13e865..cae54c935 100644
Binary files a/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png and b/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png differ
diff --git a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png
index 4720f03dd..8e526e667 100644
Binary files a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png and b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png differ
diff --git a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png
index 1998f03ba..27e4a80c2 100644
Binary files a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png and b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png differ
diff --git a/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png b/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png
index efc27d35e..6220bf70b 100644
Binary files a/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png and b/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png b/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png
index 5fb657325..1390e8955 100644
Binary files a/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png and b/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png b/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png
index a1e516982..23afb593e 100644
Binary files a/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png and b/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png b/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png
index 79ef80698..81f828387 100644
Binary files a/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png and b/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png differ
diff --git a/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png b/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png
index 95ef71285..6fb6be487 100644
Binary files a/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png and b/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png differ
diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png
index 4ccd3220b..5540b2625 100644
Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png differ
diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png
index 4196e081b..6bd648e66 100644
Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png differ
diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png
index c1e77821d..2489d3f9e 100644
Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png differ
diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png
index 91445e317..968e4e3c4 100644
Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png differ
diff --git a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png
index d4a71a14f..5506d4edc 100644
Binary files a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png and b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png differ
diff --git a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png
index c19375d9d..2aa8815ff 100644
Binary files a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png and b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png differ
diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png b/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png
index 72d6460c9..3cbedc4b5 100644
Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png differ
diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png b/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png
index 94b903480..7db768fff 100644
Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png differ
diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png b/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png
index 584bc4e57..0fc60e7ac 100644
Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png differ
diff --git a/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png b/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png
index ee866a42d..664352d20 100644
Binary files a/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png and b/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/TMTVRendering.spec.ts/tmtvDisplayedCorrectly.png b/tests/screenshots/chromium/TMTVRendering.spec.ts/tmtvDisplayedCorrectly.png
deleted file mode 100644
index 662088835..000000000
Binary files a/tests/screenshots/chromium/TMTVRendering.spec.ts/tmtvDisplayedCorrectly.png and /dev/null differ
diff --git a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png
index ed1bf15de..acf9a3a82 100644
Binary files a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png and b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png differ
diff --git a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png
index 8f6575b72..236f4f643 100644
Binary files a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png and b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png differ
diff --git a/yarn.lock b/yarn.lock
index 245782a8f..6d945776f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1393,25 +1393,25 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
-"@cornerstonejs/adapters@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.12.2.tgz#835ed61cfc26257af4a37f03023f539eaeca59d1"
- integrity sha512-yKPT9U1/CUtXu/h3xXB8KYX3x3bHgedw3nPX6eOn8oDhynfjuvA+i+4xJxKtO182mi2WnAQwFPnzkgmEVyrSqA==
+"@cornerstonejs/adapters@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.15.1.tgz#b55e51ff0105ded60d2c45eeefe0b8bd4ce63cbc"
+ integrity sha512-nwUPk2SXukPkkKrpzAOIOMI0lNqwTl1euI1iTLP1DB1gt8r2wy1V1vYu7+TVT9CfifhfJ+yrJnYCo1VwqnnPDA==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3"
- dcmjs "^0.29.8"
+ dcmjs "^0.40.0"
gl-matrix "^3.4.3"
ndarray "^1.0.19"
-"@cornerstonejs/ai@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.12.2.tgz#8e55d96f15941218f0c3b5af0c992806b9a2f2d7"
- integrity sha512-Btyey9KNcJqzIG7trFqDHo08zJVO3XtVEbfbJiUfRoztwQTKrtxOJ40dsz3pU3+PNQgG0bmTPhe9yRSpcVdwRA==
+"@cornerstonejs/ai@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.15.1.tgz#61f31a6108ef9172c602d10eb4ed67568dfe328b"
+ integrity sha512-rFcS8Hcu7Bjr+BLYMR8Y0m/eno+ynEE3Wd2gM48VNV5pXQ4HfjbA4kl7r4JyZKqyPZYgV2Api42v0vKLeSSZ6g==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3"
- dcmjs "^0.29.8"
+ dcmjs "^0.40.0"
gl-matrix "^3.4.3"
lodash.clonedeep "^4.5.0"
ndarray "^1.0.19"
@@ -1443,20 +1443,20 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0"
integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg==
-"@cornerstonejs/core@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.12.2.tgz#eeba299b27cca3664e760a9a83aa58dd0d6dc305"
- integrity sha512-AKFHr7XF5zKUo1BiclJmUz913yD9NiQgLl3krTmvtlWPuOUhpxoCfipW0De5dMeVuQ86iQDV1F+p7HuWOWfdwg==
+"@cornerstonejs/core@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.15.1.tgz#a856ea98973b30acf5071563c039425a73a958ea"
+ integrity sha512-yXYjNyAlbKOfx6Xjd2xXnRuFrOCEHRP3yC6SQDywp8/WJ4aIiIXqU8Hgxht+/22UDjjoe5DcF2H+d5yX6SZLMg==
dependencies:
"@kitware/vtk.js" "32.12.1"
comlink "^4.4.1"
gl-matrix "^3.4.3"
loglevel "^1.9.2"
-"@cornerstonejs/dicom-image-loader@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.12.2.tgz#ebf0915baed75ed37d38285f06c1a8e3cba7372d"
- integrity sha512-LaKnv5OPMlRPzkuqReVl1Gpa9ERTQJXl/1/Iku9wbWjnGrkzdF4Ef8bG0DseLx57qMOJPJP++PsxeXCaOgXvyw==
+"@cornerstonejs/dicom-image-loader@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.15.1.tgz#5fa30aeb1824e4875564454cc16c6dc6a943ab70"
+ integrity sha512-/L2pL8121Vx4OgCw4R5ZxLLfZbBSGTItWASFDeImlT9jI++KHnOpVQRugYHChMj4dHb45yX8dAify+AhSMNkXg==
dependencies:
"@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
@@ -1468,25 +1468,25 @@
pako "^2.0.4"
uuid "^9.0.0"
-"@cornerstonejs/labelmap-interpolation@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.12.2.tgz#e1682f97592b09a5246e33b4ab110a694b2f281b"
- integrity sha512-5T2OtNyV2jMy66k2clG/Krk1x4xFOxlS9sGxT1eMOrELClzpY+cL+IuA6f0aD6vTwkp1fFojEIa0IFbwuCK8HQ==
+"@cornerstonejs/labelmap-interpolation@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.15.1.tgz#948e6cebb68264e87ddca0e44308cbcee3abd804"
+ integrity sha512-94zxc91G9V+Dp3dodqSVde5ia9sUZNaP7Eud3Tsyej4unu4frIIx2/fRGVNSdq3pLNM7MT0KLfGeut+I7S3cHQ==
dependencies:
"@itk-wasm/morphological-contour-interpolation" "1.1.0"
itk-wasm "1.0.0-b.165"
-"@cornerstonejs/polymorphic-segmentation@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.12.2.tgz#80be77b1d46ff5c6edd8346099d239cf6504298a"
- integrity sha512-pmOpmOKUaAM3EIklZLufWJ9IVTRpp54MF+B6cnirGEYYobjFT+uTrLwrExwVC1kEVHi8o4n5X8SJnawtIu/ugA==
+"@cornerstonejs/polymorphic-segmentation@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.15.1.tgz#b6bea5e93d5b3a02a0aa0ce0fbd3175e6f7a90da"
+ integrity sha512-C/A/SLhOkUi0I7yXkWwu96LhaajQSIcMZX6kmU2KEuBIwRNZ2oaZm4vDF2srTJZtxKeJ/gaD0BJEn5LJFYHFfw==
dependencies:
"@icr/polyseg-wasm" "0.4.0"
-"@cornerstonejs/tools@^3.12.2":
- version "3.12.2"
- resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.12.2.tgz#6db5e1ea6cc986a8a647fd64bea05e3dcb15fb46"
- integrity sha512-Zx9BFl2OAuCQGrr6o+e1tV3fMSoAARC7HlVWprUrGWwDA29LVRxQT+cc+Wo76GjxcKSMo5RuLt1+sgRj0+CsAA==
+"@cornerstonejs/tools@^3.15.1":
+ version "3.15.1"
+ resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.15.1.tgz#db9f3ffeaa507fa234434328c0d58069cfe60e0b"
+ integrity sha512-QyowzqiitrwFomZGKvIeAbVMpjlzPjOOiMkLZUAGoSvLCBCjS9zDZFtnZ33cRgIqzVrzmVFWAn1Fi3fzqugQZQ==
dependencies:
"@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1"
@@ -1605,7 +1605,7 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016"
integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==
-"@cypress/request@^3.0.7":
+"@cypress/request@^3.0.8":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.8.tgz#992f1f42ba03ebb14fa5d97290abe9d015ed0815"
integrity sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==
@@ -7862,7 +7862,7 @@ cli-spinners@^2.5.0:
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41"
integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==
-cli-table3@^0.6.1, cli-table3@~0.6.1:
+cli-table3@^0.6.1, cli-table3@~0.6.5:
version "0.6.5"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f"
integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==
@@ -8734,12 +8734,12 @@ cypress-file-upload@^5.0.8:
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==
-cypress@^14.1.0:
- version "14.1.0"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.1.0.tgz#b2dbe7bbc529dc0c93ffd4e0e9fa59763afba0b8"
- integrity sha512-pPPj8Uu9NwjaaiXAEcjYZZmgsq6v9Zs1Nw6a+zRF+ANgYSNhH4S32SjFRsvMcuOHR/8dp4GBJhBPqIPSs+TxaA==
+cypress@14.3.1:
+ version "14.3.1"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.1.tgz#b0570c0e5b198d930a2c0f640d099e777bec2d2f"
+ integrity sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg==
dependencies:
- "@cypress/request" "^3.0.7"
+ "@cypress/request" "^3.0.8"
"@cypress/xvfb" "^1.2.4"
"@types/sinonjs__fake-timers" "8.1.1"
"@types/sizzle" "^2.3.2"
@@ -8752,7 +8752,7 @@ cypress@^14.1.0:
check-more-types "^2.24.0"
ci-info "^4.1.0"
cli-cursor "^3.1.0"
- cli-table3 "~0.6.1"
+ cli-table3 "~0.6.5"
commander "^6.2.1"
common-tags "^1.8.0"
dayjs "^1.10.4"
@@ -8776,7 +8776,7 @@ cypress@^14.1.0:
process "^0.11.10"
proxy-from-env "1.0.0"
request-progress "^3.0.0"
- semver "^7.5.3"
+ semver "^7.7.1"
supports-color "^8.1.1"
tmp "~0.2.3"
tree-kill "1.2.2"
@@ -9011,6 +9011,19 @@ dcmjs@^0.29.8:
ndarray "^1.0.19"
pako "^2.0.4"
+dcmjs@^0.40.0:
+ version "0.40.0"
+ resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.40.0.tgz#04218d221b4b197d36580301ce7e8110cd99aa50"
+ integrity sha512-FId8dPpDg45ynIjpWF1mXlA77YcQBcE/eQGgkQpJqB0k9NF4simMYbSm9pJ5ORizYdDzOzjZpVNVym/vnFQJzQ==
+ dependencies:
+ "@babel/runtime-corejs3" "^7.22.5"
+ adm-zip "^0.5.10"
+ gl-matrix "^3.1.0"
+ lodash.clonedeep "^4.5.0"
+ loglevel "^1.8.1"
+ ndarray "^1.0.19"
+ pako "^2.0.4"
+
debounce@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
@@ -19169,6 +19182,11 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semve
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+semver@^7.7.1:
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"