2026-03-17 13:59:47 +01:00
|
|
|
import { Types, DicomMetadataStore, utils } from '@ohif/core';
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
import { datasetToDicomBlob, setNonEnumerableInstanceProperty } from './utils/dicomWriter';
|
|
|
|
|
import { registerNaturalizedDatasetsForLocalWadouri } from './utils/registerNaturalizedDatasetForLocalWadouri';
|
2026-03-17 13:59:47 +01:00
|
|
|
|
|
|
|
|
const { downloadBlob } = utils;
|
2023-03-15 17:41:41 +01:00
|
|
|
|
2025-01-23 20:24:13 +01:00
|
|
|
import { ContextMenuController } from './CustomizableContextMenu';
|
2023-03-15 17:41:41 +01:00
|
|
|
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
|
|
|
|
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
2025-05-05 22:35:05 +02:00
|
|
|
import {
|
|
|
|
|
configureViewportForLayerAddition,
|
|
|
|
|
configureViewportForLayerRemoval,
|
|
|
|
|
canAddDisplaySetToViewport,
|
|
|
|
|
DERIVED_OVERLAY_MODALITIES,
|
|
|
|
|
} from './utils/layerConfigurationUtils';
|
2023-03-15 17:41:41 +01:00
|
|
|
import findViewportsByPosition, {
|
|
|
|
|
findOrCreateViewport as layoutFindOrCreate,
|
|
|
|
|
} from './findViewportsByPosition';
|
|
|
|
|
|
2023-05-29 15:04:49 +02:00
|
|
|
import { ContextMenuProps } from './CustomizableContextMenu/types';
|
2023-04-27 16:46:23 +02:00
|
|
|
import { NavigateHistory } from './types/commandModuleTypes';
|
2023-06-06 19:34:26 +02:00
|
|
|
import { history } from '@ohif/app';
|
2024-11-06 13:15:27 +01:00
|
|
|
import { useViewportGridStore } from './stores/useViewportGridStore';
|
|
|
|
|
import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore';
|
|
|
|
|
import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
|
|
|
|
|
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
|
|
|
|
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
|
|
|
|
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
2025-01-13 16:49:08 +01:00
|
|
|
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
2025-03-20 14:22:45 +01:00
|
|
|
import promptSaveReport from './utils/promptSaveReport';
|
2023-03-22 23:04:44 +01:00
|
|
|
|
2023-03-15 17:41:41 +01:00
|
|
|
export type HangingProtocolParams = {
|
|
|
|
|
protocolId?: string;
|
|
|
|
|
stageIndex?: number;
|
|
|
|
|
activeStudyUID?: string;
|
|
|
|
|
stageId?: string;
|
2024-03-27 21:01:32 +01:00
|
|
|
reset?: false;
|
2023-03-15 17:41:41 +01:00
|
|
|
};
|
|
|
|
|
|
2023-05-23 21:03:55 +02:00
|
|
|
export type UpdateViewportDisplaySetParams = {
|
|
|
|
|
direction: number;
|
|
|
|
|
excludeNonImageModalities?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-22 22:45:28 +01:00
|
|
|
const commandsModule = ({
|
|
|
|
|
servicesManager,
|
|
|
|
|
commandsManager,
|
2025-01-13 16:49:08 +01:00
|
|
|
extensionManager,
|
2023-03-22 22:45:28 +01:00
|
|
|
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
2023-03-15 17:41:41 +01:00
|
|
|
const {
|
2023-03-22 22:45:28 +01:00
|
|
|
customizationService,
|
2023-03-15 17:41:41 +01:00
|
|
|
measurementService,
|
|
|
|
|
hangingProtocolService,
|
|
|
|
|
uiNotificationService,
|
|
|
|
|
viewportGridService,
|
|
|
|
|
displaySetService,
|
2025-01-13 16:49:08 +01:00
|
|
|
multiMonitorService,
|
2024-05-08 18:12:49 +02:00
|
|
|
} = servicesManager.services;
|
2023-03-15 17:41:41 +01:00
|
|
|
|
2023-03-22 22:45:28 +01:00
|
|
|
// Define a context menu controller for use with any context menus
|
2023-09-01 22:19:39 +02:00
|
|
|
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
|
2023-03-22 22:45:28 +01:00
|
|
|
|
2023-03-15 17:41:41 +01:00
|
|
|
const actions = {
|
2025-05-05 22:35:05 +02:00
|
|
|
/**
|
|
|
|
|
* Adds a display set as a layer to the specified viewport
|
|
|
|
|
*
|
|
|
|
|
* @param options.viewportId - The ID of the viewport to add the layer to
|
|
|
|
|
* @param options.displaySetInstanceUID - The UID of the display set to add as a layer
|
|
|
|
|
* @param options.removeFirst - Optional flag to remove the display set first if it's already added
|
|
|
|
|
*/
|
|
|
|
|
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
|
2025-10-02 23:05:45 +02:00
|
|
|
if (!viewportId) {
|
2026-02-19 02:31:43 +01:00
|
|
|
const { activeViewportId } = servicesManager.services.viewportGridService.getState();
|
|
|
|
|
viewportId = activeViewportId;
|
2025-10-02 23:05:45 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-05 22:35:05 +02:00
|
|
|
if (!viewportId || !displaySetInstanceUID) {
|
|
|
|
|
console.warn('Missing required parameters for addDisplaySetAsLayer command');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { displaySetService, viewportGridService, hangingProtocolService } =
|
|
|
|
|
servicesManager.services;
|
|
|
|
|
|
|
|
|
|
// Get the display set
|
|
|
|
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
|
|
|
|
if (!displaySet) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current display sets for the viewport
|
|
|
|
|
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
|
|
|
|
|
|
|
|
|
// Check if we can add this display set to the viewport
|
|
|
|
|
const canAdd = canAddDisplaySetToViewport({
|
|
|
|
|
viewportId,
|
|
|
|
|
displaySetInstanceUID,
|
|
|
|
|
servicesManager,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!canAdd) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the display set to the viewport
|
|
|
|
|
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
|
|
|
|
viewportId,
|
|
|
|
|
displaySetInstanceUID
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Configure each viewport for layer addition
|
|
|
|
|
updatedViewports.forEach(viewport => {
|
|
|
|
|
configureViewportForLayerAddition({
|
|
|
|
|
viewport,
|
|
|
|
|
displaySetInstanceUID,
|
|
|
|
|
currentDisplaySetUIDs,
|
|
|
|
|
servicesManager,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update position presentation
|
|
|
|
|
commandsManager.runCommand('updateStoredPositionPresentation', {
|
|
|
|
|
viewportId,
|
|
|
|
|
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Run command to update viewports
|
|
|
|
|
commandsManager.run('setDisplaySetsForViewports', {
|
|
|
|
|
viewportsToUpdate: updatedViewports,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Removes a display set layer from the specified viewport
|
|
|
|
|
*
|
|
|
|
|
* @param options.viewportId - The ID of the viewport to remove the layer from
|
|
|
|
|
* @param options.displaySetInstanceUID - The UID of the display set to remove
|
|
|
|
|
*/
|
|
|
|
|
removeDisplaySetLayer: ({ viewportId, displaySetInstanceUID }) => {
|
|
|
|
|
if (!viewportId || !displaySetInstanceUID) {
|
|
|
|
|
console.warn('Missing required parameters for removeDisplaySetLayer command');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
displaySetService,
|
|
|
|
|
viewportGridService,
|
|
|
|
|
hangingProtocolService,
|
|
|
|
|
segmentationService,
|
|
|
|
|
} = servicesManager.services;
|
|
|
|
|
|
|
|
|
|
// Get the display set
|
|
|
|
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
|
|
|
|
if (!displaySet) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 02:31:43 +01:00
|
|
|
// Check if it's a segmentation and handle accordingly.
|
|
|
|
|
// Note that for the sake of hydrated segmentations, we remove the
|
|
|
|
|
// segmentation before checking if the display set is indeed in the viewport.
|
|
|
|
|
// This is because hydrated segmentations are not in the viewport per se
|
|
|
|
|
// {i.e. they are not layered) but are simply referenced by the display
|
|
|
|
|
// set in the viewport.
|
|
|
|
|
const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality);
|
|
|
|
|
if (isSegmentation) {
|
|
|
|
|
segmentationService.removeRepresentationsFromViewport(viewportId, {
|
|
|
|
|
segmentationId: displaySetInstanceUID,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-05 22:35:05 +02:00
|
|
|
// Get current display sets for the viewport
|
|
|
|
|
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
|
|
|
|
|
|
|
|
|
// If the display set is not in the viewport, no need to remove it
|
|
|
|
|
if (!currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
|
|
|
|
viewportId,
|
|
|
|
|
displaySetInstanceUID
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Configure each viewport for layer removal
|
|
|
|
|
updatedViewports.forEach(viewport => {
|
|
|
|
|
configureViewportForLayerRemoval({
|
|
|
|
|
viewport,
|
|
|
|
|
displaySetInstanceUID,
|
|
|
|
|
currentDisplaySetUIDs,
|
|
|
|
|
servicesManager,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update position presentation
|
|
|
|
|
commandsManager.runCommand('updateStoredPositionPresentation', {
|
|
|
|
|
viewportId,
|
|
|
|
|
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update the viewports
|
|
|
|
|
commandsManager.run('setDisplaySetsForViewports', {
|
|
|
|
|
viewportsToUpdate: updatedViewports,
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-01-13 16:49:08 +01:00
|
|
|
/**
|
|
|
|
|
* Runs a command in multi-monitor mode. No-op if not multi-monitor.
|
|
|
|
|
*/
|
|
|
|
|
multimonitor: async options => {
|
|
|
|
|
const { screenDelta, StudyInstanceUID, commands, hashParams } = options;
|
|
|
|
|
if (multiMonitorService.numberOfScreens < 2) {
|
|
|
|
|
return options.fallback?.(options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newWindow = await multiMonitorService.launchWindow(
|
|
|
|
|
StudyInstanceUID,
|
|
|
|
|
screenDelta,
|
|
|
|
|
hashParams
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Only run commands if we successfully got a window with a commands manager
|
|
|
|
|
if (newWindow && commands) {
|
|
|
|
|
// Todo: fix this properly, but it takes time for the new window to load
|
|
|
|
|
// and then the commandsManager is available for it
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
multiMonitorService.run(screenDelta, commands, options);
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2025-03-20 14:22:45 +01:00
|
|
|
/** Displays a prompt and then save the report if relevant */
|
|
|
|
|
promptSaveReport: props => {
|
|
|
|
|
const { StudyInstanceUID } = props;
|
|
|
|
|
promptSaveReport({ servicesManager, commandsManager, extensionManager }, props, {
|
|
|
|
|
data: { StudyInstanceUID },
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2025-01-13 16:49:08 +01:00
|
|
|
/**
|
|
|
|
|
* Ensures that the specified study is available for display
|
|
|
|
|
* Then, if commands is specified, runs the given commands list/instance
|
|
|
|
|
*/
|
|
|
|
|
loadStudy: async options => {
|
|
|
|
|
const { StudyInstanceUID } = options;
|
|
|
|
|
const displaySets = displaySetService.getActiveDisplaySets();
|
|
|
|
|
const isActive = displaySets.find(ds => ds.StudyInstanceUID === StudyInstanceUID);
|
|
|
|
|
if (isActive) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const [dataSource] = extensionManager.getActiveDataSource();
|
|
|
|
|
await requestDisplaySetCreationForStudy(dataSource, displaySetService, StudyInstanceUID);
|
|
|
|
|
|
|
|
|
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
|
|
|
|
hangingProtocolService.addStudy(study);
|
|
|
|
|
},
|
|
|
|
|
|
2023-03-22 22:45:28 +01:00
|
|
|
/**
|
|
|
|
|
* Show the context menu.
|
|
|
|
|
* @param options.menuId defines the menu name to lookup, from customizationService
|
|
|
|
|
* @param options.defaultMenu contains the default menu set to use
|
|
|
|
|
* @param options.element is the element to show the menu within
|
|
|
|
|
* @param options.event is the event that caused the context menu
|
|
|
|
|
* @param options.selectorProps is the set of selection properties to use
|
|
|
|
|
*/
|
2023-03-22 23:04:44 +01:00
|
|
|
showContextMenu: (options: ContextMenuProps) => {
|
2023-03-22 22:45:28 +01:00
|
|
|
const {
|
|
|
|
|
menuCustomizationId,
|
|
|
|
|
element,
|
|
|
|
|
event,
|
|
|
|
|
selectorProps,
|
|
|
|
|
defaultPointsPosition = [],
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
const optionsToUse = { ...options };
|
|
|
|
|
|
|
|
|
|
if (menuCustomizationId) {
|
2025-01-23 20:24:13 +01:00
|
|
|
Object.assign(optionsToUse, customizationService.getCustomization(menuCustomizationId));
|
2023-03-22 22:45:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO - make the selectorProps richer by including the study metadata and display set.
|
|
|
|
|
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
|
|
|
|
|
optionsToUse.selectorProps = {
|
|
|
|
|
event,
|
|
|
|
|
protocol,
|
|
|
|
|
stage,
|
|
|
|
|
...selectorProps,
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
contextMenuController.showContextMenu(optionsToUse, element, defaultPointsPosition);
|
2023-03-22 22:45:28 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Close a context menu currently displayed */
|
|
|
|
|
closeContextMenu: () => {
|
|
|
|
|
contextMenuController.closeContextMenu();
|
|
|
|
|
},
|
|
|
|
|
|
2023-03-15 17:41:41 +01:00
|
|
|
displayNotification: ({ text, title, type }) => {
|
|
|
|
|
uiNotificationService.show({
|
|
|
|
|
title: title,
|
|
|
|
|
message: text,
|
|
|
|
|
type: type,
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-03-20 14:22:45 +01:00
|
|
|
|
|
|
|
|
clearMeasurements: options => {
|
|
|
|
|
measurementService.clearMeasurements(options.measurementFilter);
|
2023-03-15 17:41:41 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets the specified protocol
|
|
|
|
|
* 1. Records any existing state using the viewport grid service
|
|
|
|
|
* 2. Finds the destination state - this can be one of:
|
|
|
|
|
* a. The specified protocol stage
|
|
|
|
|
* b. An alternate (toggled or restored) protocol stage
|
|
|
|
|
* c. A restored custom layout
|
|
|
|
|
* 3. Finds the parameters for the specified state
|
|
|
|
|
* a. Gets the displaySetSelectorMap
|
|
|
|
|
* b. Gets the map by position
|
|
|
|
|
* c. Gets any toggle mapping to map position to/from current view
|
|
|
|
|
* 4. If restore, then sets layout
|
|
|
|
|
* a. Maps viewport position by currently displayed viewport map id
|
|
|
|
|
* b. Uses toggle information to map display set id
|
|
|
|
|
* 5. Else applies the hanging protocol
|
|
|
|
|
* a. HP Service is provided displaySetSelectorMap
|
|
|
|
|
* b. HP Service will throw an exception if it isn't applicable
|
|
|
|
|
* @param options - contains information on the HP to apply
|
|
|
|
|
* @param options.activeStudyUID - the updated study to apply the HP to
|
|
|
|
|
* @param options.protocolId - the protocol ID to change to
|
|
|
|
|
* @param options.stageId - the stageId to apply
|
|
|
|
|
* @param options.stageIndex - the index of the stage to go to.
|
2023-03-29 21:39:51 +02:00
|
|
|
* @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state
|
2024-11-28 18:41:08 +01:00
|
|
|
*
|
|
|
|
|
* commandsManager.run('setHangingProtocol', {
|
|
|
|
|
* activeStudyUID: '1.2.3',
|
|
|
|
|
* protocolId: 'myProtocol',
|
|
|
|
|
* stageId: 'myStage',
|
|
|
|
|
* stageIndex: 0,
|
|
|
|
|
* reset: false,
|
|
|
|
|
* });
|
2023-03-15 17:41:41 +01:00
|
|
|
*/
|
|
|
|
|
setHangingProtocol: ({
|
|
|
|
|
activeStudyUID = '',
|
2025-01-13 16:49:08 +01:00
|
|
|
StudyInstanceUID = '',
|
2023-03-15 17:41:41 +01:00
|
|
|
protocolId,
|
|
|
|
|
stageId,
|
|
|
|
|
stageIndex,
|
2023-03-29 21:39:51 +02:00
|
|
|
reset = false,
|
2023-03-15 17:41:41 +01:00
|
|
|
}: HangingProtocolParams): boolean => {
|
2025-01-13 16:49:08 +01:00
|
|
|
const toUseStudyInstanceUID = activeStudyUID || StudyInstanceUID;
|
2023-03-15 17:41:41 +01:00
|
|
|
try {
|
2023-04-18 17:55:25 +02:00
|
|
|
// Stores in the state the display set selector id to displaySetUID mapping
|
2023-03-15 17:41:41 +01:00
|
|
|
// Pass in viewportId for the active viewport. This item will get set as
|
|
|
|
|
// the activeViewportId
|
|
|
|
|
const state = viewportGridService.getState();
|
|
|
|
|
const hpInfo = hangingProtocolService.getState();
|
2024-11-06 13:15:27 +01:00
|
|
|
reuseCachedLayouts(state, hangingProtocolService);
|
|
|
|
|
const { hangingProtocolStageIndexMap } = useHangingProtocolStageIndexStore.getState();
|
|
|
|
|
const { displaySetSelectorMap } = useDisplaySetSelectorStore.getState();
|
2023-03-15 17:41:41 +01:00
|
|
|
|
|
|
|
|
if (!protocolId) {
|
2023-09-12 13:40:38 +02:00
|
|
|
// Reuse the previous protocol id, and optionally stage
|
2023-03-15 17:41:41 +01:00
|
|
|
protocolId = hpInfo.protocolId;
|
|
|
|
|
if (stageId === undefined && stageIndex === undefined) {
|
|
|
|
|
stageIndex = hpInfo.stageIndex;
|
|
|
|
|
}
|
|
|
|
|
} else if (stageIndex === undefined && stageId === undefined) {
|
|
|
|
|
// Re-set the same stage as was previously used
|
2025-01-13 16:49:08 +01:00
|
|
|
const hangingId = `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:${protocolId}`;
|
2023-03-15 17:41:41 +01:00
|
|
|
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const useStageIdx =
|
|
|
|
|
stageIndex ??
|
|
|
|
|
hangingProtocolService.getStageIndex(protocolId, {
|
|
|
|
|
stageId,
|
|
|
|
|
stageIndex,
|
|
|
|
|
});
|
|
|
|
|
|
2025-01-13 16:49:08 +01:00
|
|
|
const activeStudyChanged = hangingProtocolService.setActiveStudyUID(toUseStudyInstanceUID);
|
2023-03-15 17:41:41 +01:00
|
|
|
|
2025-01-13 16:49:08 +01:00
|
|
|
const storedHanging = `${toUseStudyInstanceUID || hangingProtocolService.getState().activeStudyUID}:${protocolId}:${
|
2023-09-01 22:19:39 +02:00
|
|
|
useStageIdx || 0
|
|
|
|
|
}`;
|
2023-03-15 17:41:41 +01:00
|
|
|
|
2024-11-06 13:15:27 +01:00
|
|
|
const { viewportGridState } = useViewportGridStore.getState();
|
|
|
|
|
const restoreProtocol = !reset && viewportGridState[storedHanging];
|
2023-03-15 17:41:41 +01:00
|
|
|
|
|
|
|
|
if (
|
2025-01-13 16:49:08 +01:00
|
|
|
reset ||
|
|
|
|
|
(activeStudyChanged &&
|
|
|
|
|
!viewportGridState[storedHanging] &&
|
|
|
|
|
stageIndex === undefined &&
|
|
|
|
|
stageId === undefined)
|
|
|
|
|
) {
|
|
|
|
|
// Run the hanging protocol fresh, re-using the existing study data
|
|
|
|
|
// This is done on reset or when the study changes and we haven't yet
|
|
|
|
|
// applied it, and don't specify exact stage to use.
|
|
|
|
|
const displaySets = displaySetService.getActiveDisplaySets();
|
|
|
|
|
const activeStudy = {
|
|
|
|
|
StudyInstanceUID: toUseStudyInstanceUID,
|
|
|
|
|
displaySets,
|
|
|
|
|
};
|
|
|
|
|
hangingProtocolService.run(activeStudy, protocolId);
|
|
|
|
|
} else if (
|
2023-03-20 20:11:21 +01:00
|
|
|
protocolId === hpInfo.protocolId &&
|
|
|
|
|
useStageIdx === hpInfo.stageIndex &&
|
2025-01-13 16:49:08 +01:00
|
|
|
!toUseStudyInstanceUID
|
2023-03-15 17:41:41 +01:00
|
|
|
) {
|
|
|
|
|
// Clear the HP setting to reset them
|
|
|
|
|
hangingProtocolService.setProtocol(protocolId, {
|
|
|
|
|
stageId,
|
|
|
|
|
stageIndex: useStageIdx,
|
2025-06-13 16:43:31 +02:00
|
|
|
displaySetSelectorMap,
|
2023-03-15 17:41:41 +01:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
hangingProtocolService.setProtocol(protocolId, {
|
|
|
|
|
displaySetSelectorMap,
|
|
|
|
|
stageId,
|
|
|
|
|
stageIndex: useStageIdx,
|
|
|
|
|
restoreProtocol,
|
|
|
|
|
});
|
|
|
|
|
if (restoreProtocol) {
|
2024-11-06 13:15:27 +01:00
|
|
|
viewportGridService.set(viewportGridState[storedHanging]);
|
2023-03-15 17:41:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Do this after successfully applying the update
|
2024-11-06 13:15:27 +01:00
|
|
|
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
|
|
|
|
|
setDisplaySetSelector(
|
2025-01-13 16:49:08 +01:00
|
|
|
`${toUseStudyInstanceUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
|
2024-11-06 13:15:27 +01:00
|
|
|
null
|
|
|
|
|
);
|
2023-03-15 17:41:41 +01:00
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
2023-10-06 05:39:56 +02:00
|
|
|
console.error(e);
|
2023-03-15 17:41:41 +01:00
|
|
|
uiNotificationService.show({
|
|
|
|
|
title: 'Apply Hanging Protocol',
|
2023-03-21 19:25:56 +01:00
|
|
|
message: 'The hanging protocol could not be applied.',
|
2023-03-15 17:41:41 +01:00
|
|
|
type: 'error',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
toggleHangingProtocol: ({ protocolId, stageIndex }: HangingProtocolParams): boolean => {
|
2023-03-15 17:41:41 +01:00
|
|
|
const {
|
|
|
|
|
protocol,
|
|
|
|
|
stageIndex: desiredStageIndex,
|
|
|
|
|
activeStudy,
|
|
|
|
|
} = hangingProtocolService.getActiveProtocol();
|
2024-11-06 13:15:27 +01:00
|
|
|
const { toggleHangingProtocol, setToggleHangingProtocol } =
|
|
|
|
|
useToggleHangingProtocolStore.getState();
|
2023-09-01 22:19:39 +02:00
|
|
|
const storedHanging = `${activeStudy.StudyInstanceUID}:${protocolId}:${stageIndex | 0}`;
|
2023-03-15 17:41:41 +01:00
|
|
|
if (
|
|
|
|
|
protocol.id === protocolId &&
|
|
|
|
|
(stageIndex === undefined || stageIndex === desiredStageIndex)
|
|
|
|
|
) {
|
|
|
|
|
// Toggling off - restore to previous state
|
|
|
|
|
const previousState = toggleHangingProtocol[storedHanging] || {
|
|
|
|
|
protocolId: 'default',
|
|
|
|
|
};
|
|
|
|
|
return actions.setHangingProtocol(previousState);
|
|
|
|
|
} else {
|
2024-11-06 13:15:27 +01:00
|
|
|
setToggleHangingProtocol(storedHanging, {
|
|
|
|
|
protocolId: protocol.id,
|
|
|
|
|
stageIndex: desiredStageIndex,
|
2023-03-15 17:41:41 +01:00
|
|
|
});
|
2023-03-29 21:39:51 +02:00
|
|
|
return actions.setHangingProtocol({
|
|
|
|
|
protocolId,
|
|
|
|
|
stageIndex,
|
|
|
|
|
reset: true,
|
|
|
|
|
});
|
2023-03-15 17:41:41 +01:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
deltaStage: ({ direction }) => {
|
2023-09-01 22:19:39 +02:00
|
|
|
const { protocolId, stageIndex: oldStageIndex } = hangingProtocolService.getState();
|
2023-03-15 17:41:41 +01:00
|
|
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
|
|
|
|
for (
|
|
|
|
|
let stageIndex = oldStageIndex + direction;
|
|
|
|
|
stageIndex >= 0 && stageIndex < protocol.stages.length;
|
|
|
|
|
stageIndex += direction
|
|
|
|
|
) {
|
|
|
|
|
if (protocol.stages[stageIndex].status !== 'disabled') {
|
|
|
|
|
return actions.setHangingProtocol({
|
|
|
|
|
protocolId,
|
|
|
|
|
stageIndex,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
uiNotificationService.show({
|
|
|
|
|
title: 'Change Stage',
|
|
|
|
|
message: 'The hanging protocol has no more applicable stages',
|
2023-03-30 16:01:48 +02:00
|
|
|
type: 'info',
|
2023-03-15 17:41:41 +01:00
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Changes the viewport grid layout in terms of the MxN layout.
|
|
|
|
|
*/
|
2024-04-16 17:04:48 +02:00
|
|
|
setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => {
|
2023-03-15 17:41:41 +01:00
|
|
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
|
|
|
|
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
|
|
|
|
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
|
|
|
|
// Don't apply the layout if the run command returns false
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const completeLayout = () => {
|
|
|
|
|
const state = viewportGridService.getState();
|
2024-11-06 13:15:27 +01:00
|
|
|
findViewportsByPosition(state, { numRows, numCols });
|
|
|
|
|
|
|
|
|
|
const { viewportsByPosition, initialInDisplay } = useViewportsByPositionStore.getState();
|
|
|
|
|
|
2023-03-15 17:41:41 +01:00
|
|
|
const findOrCreateViewport = layoutFindOrCreate.bind(
|
|
|
|
|
null,
|
|
|
|
|
hangingProtocolService,
|
2024-04-19 05:21:02 +02:00
|
|
|
isHangingProtocolLayout,
|
2024-11-06 13:15:27 +01:00
|
|
|
{ ...viewportsByPosition, initialInDisplay }
|
2023-03-15 17:41:41 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
viewportGridService.setLayout({
|
|
|
|
|
numRows,
|
|
|
|
|
numCols,
|
|
|
|
|
findOrCreateViewport,
|
2024-04-16 17:04:48 +02:00
|
|
|
isHangingProtocolLayout,
|
2023-03-15 17:41:41 +01:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
// Need to finish any work in the callback
|
|
|
|
|
window.setTimeout(completeLayout, 0);
|
|
|
|
|
},
|
|
|
|
|
|
2023-03-29 21:39:51 +02:00
|
|
|
toggleOneUp() {
|
|
|
|
|
const viewportGridState = viewportGridService.getState();
|
2024-04-19 05:21:02 +02:00
|
|
|
const { activeViewportId, viewports, layout, isHangingProtocolLayout } = viewportGridState;
|
2023-09-01 22:19:39 +02:00
|
|
|
const { displaySetInstanceUIDs, displaySetOptions, viewportOptions } =
|
|
|
|
|
viewports.get(activeViewportId);
|
2023-03-29 21:39:51 +02:00
|
|
|
|
|
|
|
|
if (layout.numCols === 1 && layout.numRows === 1) {
|
|
|
|
|
// The viewer is in one-up. Check if there is a state to restore/toggle back to.
|
2024-11-06 13:15:27 +01:00
|
|
|
const { toggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
|
2023-03-29 21:39:51 +02:00
|
|
|
|
2024-11-06 13:15:27 +01:00
|
|
|
if (!toggleOneUpViewportGridStore) {
|
2023-03-29 21:39:51 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// There is a state to toggle back to. The viewport that was
|
|
|
|
|
// originally toggled to one up was the former active viewport.
|
2023-09-01 22:19:39 +02:00
|
|
|
const viewportIdToUpdate = toggleOneUpViewportGridStore.activeViewportId;
|
2023-08-30 22:46:55 +02:00
|
|
|
|
2023-09-26 22:49:06 +02:00
|
|
|
// We are restoring the previous layout but taking into the account that
|
|
|
|
|
// the current one up viewport might have a new displaySet dragged and dropped on it.
|
|
|
|
|
// updatedViewportsViaHP below contains the viewports applicable to the HP that existed
|
|
|
|
|
// prior to the toggle to one-up - including the updated viewports if a display
|
|
|
|
|
// set swap were to have occurred.
|
2023-08-30 22:46:55 +02:00
|
|
|
const updatedViewportsViaHP =
|
2023-03-29 21:39:51 +02:00
|
|
|
displaySetInstanceUIDs.length > 1
|
|
|
|
|
? []
|
|
|
|
|
: displaySetInstanceUIDs
|
|
|
|
|
.map(displaySetInstanceUID =>
|
|
|
|
|
hangingProtocolService.getViewportsRequireUpdate(
|
2023-08-30 22:46:55 +02:00
|
|
|
viewportIdToUpdate,
|
2024-04-19 05:21:02 +02:00
|
|
|
displaySetInstanceUID,
|
|
|
|
|
isHangingProtocolLayout
|
2023-03-29 21:39:51 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.flat();
|
|
|
|
|
|
2023-09-26 22:49:06 +02:00
|
|
|
// findOrCreateViewport returns either one of the updatedViewportsViaHP
|
2023-03-29 21:39:51 +02:00
|
|
|
// returned from the HP service OR if there is not one from the HP service then
|
2023-09-26 22:49:06 +02:00
|
|
|
// simply returns what was in the previous state for a given position in the layout.
|
|
|
|
|
const findOrCreateViewport = (position: number, positionId: string) => {
|
|
|
|
|
// Find the viewport for the given position prior to the toggle to one-up.
|
|
|
|
|
const preOneUpViewport = Array.from(toggleOneUpViewportGridStore.viewports.values()).find(
|
|
|
|
|
viewport => viewport.positionId === positionId
|
2023-09-01 22:19:39 +02:00
|
|
|
);
|
2023-08-30 22:46:55 +02:00
|
|
|
|
2023-09-26 22:49:06 +02:00
|
|
|
// Use the viewport id from before the toggle to one-up to find any updates to the viewport.
|
|
|
|
|
const viewport = updatedViewportsViaHP.find(
|
|
|
|
|
viewport => viewport.viewportId === preOneUpViewport.viewportId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return viewport
|
|
|
|
|
? // Use the applicable viewport from the HP updated viewports
|
|
|
|
|
{ viewportOptions, displaySetOptions, ...viewport }
|
|
|
|
|
: // Use the previous viewport for the given position
|
|
|
|
|
preOneUpViewport;
|
2023-03-29 21:39:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const layoutOptions = viewportGridService.getLayoutOptionsFromState(
|
|
|
|
|
toggleOneUpViewportGridStore
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Restore the previous layout including the active viewport.
|
|
|
|
|
viewportGridService.setLayout({
|
|
|
|
|
numRows: toggleOneUpViewportGridStore.layout.numRows,
|
|
|
|
|
numCols: toggleOneUpViewportGridStore.layout.numCols,
|
2023-08-30 22:46:55 +02:00
|
|
|
activeViewportId: viewportIdToUpdate,
|
2023-03-29 21:39:51 +02:00
|
|
|
layoutOptions,
|
|
|
|
|
findOrCreateViewport,
|
2024-04-16 17:04:48 +02:00
|
|
|
isHangingProtocolLayout: true,
|
2023-03-29 21:39:51 +02:00
|
|
|
});
|
2024-11-07 06:20:30 +01:00
|
|
|
|
|
|
|
|
// Reset crosshairs after restoring the layout
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
commandsManager.runCommand('resetCrosshairs');
|
|
|
|
|
}, 0);
|
2023-03-29 21:39:51 +02:00
|
|
|
} else {
|
|
|
|
|
// We are not in one-up, so toggle to one up.
|
|
|
|
|
|
|
|
|
|
// Store the current viewport grid state so we can toggle it back later.
|
2024-11-06 13:15:27 +01:00
|
|
|
const { setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
|
|
|
|
|
setToggleOneUpViewportGridStore(viewportGridState);
|
2023-03-29 21:39:51 +02:00
|
|
|
|
|
|
|
|
// one being toggled to one up.
|
|
|
|
|
const findOrCreateViewport = () => {
|
|
|
|
|
return {
|
|
|
|
|
displaySetInstanceUIDs,
|
|
|
|
|
displaySetOptions,
|
|
|
|
|
viewportOptions,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Set the layout to be 1x1/one-up.
|
|
|
|
|
viewportGridService.setLayout({
|
|
|
|
|
numRows: 1,
|
|
|
|
|
numCols: 1,
|
|
|
|
|
findOrCreateViewport,
|
2024-04-16 17:04:48 +02:00
|
|
|
isHangingProtocolLayout: true,
|
2023-03-29 21:39:51 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2023-04-27 16:46:23 +02:00
|
|
|
/**
|
|
|
|
|
* Exposes the browser history navigation used by OHIF. This command can be used to either replace or
|
|
|
|
|
* push a new entry into the browser history. For example, the following will replace the current
|
|
|
|
|
* browser history entry with the specified relative URL which changes the study displayed to the
|
|
|
|
|
* study with study instance UID 1.2.3. Note that as a result of using `options.replace = true`, the
|
|
|
|
|
* page prior to invoking this command cannot be returned to via the browser back button.
|
|
|
|
|
*
|
|
|
|
|
* navigateHistory({
|
|
|
|
|
* to: 'viewer?StudyInstanceUIDs=1.2.3',
|
|
|
|
|
* options: { replace: true },
|
|
|
|
|
* });
|
|
|
|
|
*
|
|
|
|
|
* @param historyArgs - arguments for the history function;
|
|
|
|
|
* the `to` property is the URL;
|
|
|
|
|
* the `options.replace` is a boolean indicating if the current browser history entry
|
|
|
|
|
* should be replaced or a new entry pushed onto the history (stack); the default value
|
|
|
|
|
* for `replace` is false
|
|
|
|
|
*/
|
|
|
|
|
navigateHistory(historyArgs: NavigateHistory) {
|
|
|
|
|
history.navigate(historyArgs.to, historyArgs.options);
|
|
|
|
|
},
|
|
|
|
|
|
2024-10-18 17:42:44 +02:00
|
|
|
openDICOMTagViewer({ displaySetInstanceUID }: { displaySetInstanceUID?: string }) {
|
2023-08-30 22:46:55 +02:00
|
|
|
const { activeViewportId, viewports } = viewportGridService.getState();
|
|
|
|
|
const activeViewportSpecificData = viewports.get(activeViewportId);
|
2023-03-15 17:41:41 +01:00
|
|
|
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
|
|
|
|
|
|
|
|
|
const displaySets = displaySetService.activeDisplaySets;
|
|
|
|
|
const { UIModalService } = servicesManager.services;
|
|
|
|
|
|
2026-02-24 14:53:05 +01:00
|
|
|
const defaultDisplaySetInstanceUID =
|
|
|
|
|
displaySetInstanceUID || displaySetInstanceUIDs[0] || displaySets[0]?.displaySetInstanceUID;
|
|
|
|
|
|
2023-03-15 17:41:41 +01:00
|
|
|
UIModalService.show({
|
|
|
|
|
content: DicomTagBrowser,
|
|
|
|
|
contentProps: {
|
|
|
|
|
displaySets,
|
2024-10-18 17:42:44 +02:00
|
|
|
displaySetInstanceUID: defaultDisplaySetInstanceUID,
|
2023-03-15 17:41:41 +01:00
|
|
|
},
|
|
|
|
|
title: 'DICOM Tag Browser',
|
2025-02-28 21:17:55 +01:00
|
|
|
containerClassName: 'max-w-3xl',
|
2023-03-15 17:41:41 +01:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Toggle viewport overlay (the information panel shown on the four corners
|
|
|
|
|
* of the viewport)
|
|
|
|
|
* @see ViewportOverlay and CustomizableViewportOverlay components
|
|
|
|
|
*/
|
|
|
|
|
toggleOverlays: () => {
|
|
|
|
|
const overlays = document.getElementsByClassName('viewport-overlay');
|
|
|
|
|
for (let i = 0; i < overlays.length; i++) {
|
|
|
|
|
overlays.item(i).classList.toggle('hidden');
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
scrollActiveThumbnailIntoView: () => {
|
2023-08-30 22:46:55 +02:00
|
|
|
const { activeViewportId, viewports } = viewportGridService.getState();
|
2023-05-23 21:03:55 +02:00
|
|
|
|
2023-08-30 22:46:55 +02:00
|
|
|
const activeViewport = viewports.get(activeViewportId);
|
2025-04-11 18:41:04 +02:00
|
|
|
const activeDisplaySetInstanceUID = activeViewport?.displaySetInstanceUIDs?.[0];
|
|
|
|
|
|
|
|
|
|
if (!activeDisplaySetInstanceUID) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
const thumbnailList = document.querySelector('#ohif-thumbnail-list');
|
|
|
|
|
|
|
|
|
|
if (!thumbnailList) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
const thumbnail = document.querySelector(`#thumbnail-${activeDisplaySetInstanceUID}`);
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
if (!thumbnail) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
thumbnail.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateViewportDisplaySet: ({
|
|
|
|
|
direction,
|
|
|
|
|
excludeNonImageModalities,
|
|
|
|
|
}: UpdateViewportDisplaySetParams) => {
|
2023-09-01 22:19:39 +02:00
|
|
|
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
const currentDisplaySets = [...displaySetService.activeDisplaySets];
|
|
|
|
|
|
2024-04-19 05:21:02 +02:00
|
|
|
const { activeViewportId, viewports, isHangingProtocolLayout } =
|
|
|
|
|
viewportGridService.getState();
|
2023-05-23 21:03:55 +02:00
|
|
|
|
2023-08-30 22:46:55 +02:00
|
|
|
const { displaySetInstanceUIDs } = viewports.get(activeViewportId);
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
const activeDisplaySetIndex = currentDisplaySets.findIndex(displaySet =>
|
|
|
|
|
displaySetInstanceUIDs.includes(displaySet.displaySetInstanceUID)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let displaySetIndexToShow: number;
|
|
|
|
|
|
|
|
|
|
for (
|
|
|
|
|
displaySetIndexToShow = activeDisplaySetIndex + direction;
|
2023-09-01 22:19:39 +02:00
|
|
|
displaySetIndexToShow > -1 && displaySetIndexToShow < currentDisplaySets.length;
|
2023-05-23 21:03:55 +02:00
|
|
|
displaySetIndexToShow += direction
|
|
|
|
|
) {
|
2026-03-29 00:20:49 +01:00
|
|
|
const nextDisplaySet = currentDisplaySets[displaySetIndexToShow];
|
|
|
|
|
if (nextDisplaySet.madeInClient) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!excludeNonImageModalities || !nonImageModalities.includes(nextDisplaySet.Modality)) {
|
2023-05-23 21:03:55 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
if (displaySetIndexToShow < 0 || displaySetIndexToShow >= currentDisplaySets.length) {
|
2023-05-23 21:03:55 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 22:19:39 +02:00
|
|
|
const { displaySetInstanceUID } = currentDisplaySets[displaySetIndexToShow];
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
let updatedViewports = [];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
2023-08-30 22:46:55 +02:00
|
|
|
activeViewportId,
|
2024-04-19 05:21:02 +02:00
|
|
|
displaySetInstanceUID,
|
|
|
|
|
isHangingProtocolLayout
|
2023-05-23 21:03:55 +02:00
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(error);
|
|
|
|
|
uiNotificationService.show({
|
|
|
|
|
title: 'Navigate Viewport Display Set',
|
|
|
|
|
message:
|
|
|
|
|
'The requested display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
|
|
|
|
|
type: 'info',
|
|
|
|
|
duration: 3000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 19:33:36 +02:00
|
|
|
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
|
2023-05-23 21:03:55 +02:00
|
|
|
|
|
|
|
|
setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0);
|
|
|
|
|
},
|
2026-03-17 13:59:47 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a store function based on the data source type.
|
|
|
|
|
* @param dataSource - 'download', 'copyToClipboard', or a named data source
|
|
|
|
|
* @param defaultFileName - Default filename for download/clipboard
|
|
|
|
|
* @param defaultContentType - Default content type for clipboard
|
|
|
|
|
* @returns A store function, or null if no valid store exists
|
|
|
|
|
*/
|
|
|
|
|
createStoreFunction: ({ dataSource, defaultFileName, defaultContentType }) => {
|
|
|
|
|
if (dataSource === 'download') {
|
|
|
|
|
return async dicom => {
|
|
|
|
|
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
registerNaturalizedDatasetsForLocalWadouri(instances);
|
2026-03-17 13:59:47 +01:00
|
|
|
DicomMetadataStore.addInstances(instances, true);
|
|
|
|
|
if (instances.length !== 1) {
|
|
|
|
|
throw new Error('Download only supports a single DICOM instance');
|
|
|
|
|
}
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
const reportBlob = datasetToDicomBlob(instances[0]);
|
2026-03-17 13:59:47 +01:00
|
|
|
downloadBlob(reportBlob, { filename: defaultFileName || 'dicom.dcm' });
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dataSource === 'copyToClipboard') {
|
|
|
|
|
return async dicom => {
|
|
|
|
|
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
registerNaturalizedDatasetsForLocalWadouri(instances);
|
2026-03-17 13:59:47 +01:00
|
|
|
DicomMetadataStore.addInstances(instances, true);
|
|
|
|
|
if (instances.length !== 1) {
|
|
|
|
|
throw new Error('Copy to clipboard only supports a single DICOM instance');
|
|
|
|
|
}
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
const reportBlob = datasetToDicomBlob(instances[0]);
|
2026-03-17 13:59:47 +01:00
|
|
|
const type = defaultContentType || 'application/dicom';
|
|
|
|
|
await navigator.clipboard.write([
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
new ClipboardItem({ [type]: reportBlob }),
|
2026-03-17 13:59:47 +01:00
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DICOM STOW path — resolve the named data source
|
|
|
|
|
const dataSources = extensionManager.getDataSources(dataSource);
|
|
|
|
|
const resolvedDataSource = dataSources?.[0];
|
|
|
|
|
if (!resolvedDataSource?.store?.dicom) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return async (dicom, { dicomDict } = {}) => {
|
|
|
|
|
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
// Always keep an in-memory wadouri copy so DICOM can be read without re-fetching.
|
|
|
|
|
registerNaturalizedDatasetsForLocalWadouri(instances);
|
|
|
|
|
|
|
|
|
|
if (dataSource !== 'dicomlocal') {
|
|
|
|
|
const config = resolvedDataSource.getConfig?.();
|
|
|
|
|
if (config?.wadoRoot) {
|
|
|
|
|
instances.forEach(instance => {
|
|
|
|
|
setNonEnumerableInstanceProperty(instance, 'wadoRoot', config.wadoRoot);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-17 13:59:47 +01:00
|
|
|
}
|
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]
* feat: load DICOM SEG images via imageLoader
* feat: load DICOM SEG images via imageLoader
* PR review fixes
* Test fragment of compressed multiframes
* fix: Removed dependencies incorrectly
* Update frame as a targetted change to avoid stripping remaining params
* lock
* Update test to agree with the missing representation branch
* test(contour): contour color change coverage (#6042)
* test(contour): contour interactions delete segment (#6069)
---------
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]
* feat(testing): OHIF Test Agent Skills (#5993)
* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]
* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)
* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]
* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]
* Debug fixes
* perf(seg): pass explicit frame decode concurrency (16) to SEG loader
Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal
Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.
First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pre-cache the image data using a full part10 file for performance
* Add customizations to specify type of segmentation save
* Add clearcache of the cacheData
* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base
libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix seg OOM
* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader
- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
group (download / clipboard / local wadouri blob / local store); keep the
hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
project dependency; the rule only serves the gitignored cs3d-link
workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* PR review comment fixes
* Update versions
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-10 02:54:23 +02:00
|
|
|
|
2026-03-17 13:59:47 +01:00
|
|
|
DicomMetadataStore.addInstances(instances, true);
|
|
|
|
|
for (const instance of instances) {
|
|
|
|
|
await resolvedDataSource.store.dicom(instance, null, dicomDict);
|
|
|
|
|
}
|
|
|
|
|
const studyUIDs = new Set(instances.map(i => i.StudyInstanceUID).filter(Boolean));
|
|
|
|
|
for (const uid of studyUIDs) {
|
|
|
|
|
resolvedDataSource.deleteStudyMetadataPromise(uid);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-07-10 14:16:16 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Launches a workflow (mode) for a study from the worklist. This is the
|
|
|
|
|
* default `workList.onStudyDoubleClick` command.
|
|
|
|
|
*
|
|
|
|
|
* @param study - the StudyRow the action applies to
|
|
|
|
|
* @param workflows - the workflows applicable to the study, in menu order;
|
|
|
|
|
* each has `id`, `displayName`, `isDefault` and `launchWithStudy(study)`
|
|
|
|
|
* @param defaultWorkflow - the user's default workflow when it applies to
|
|
|
|
|
* the study
|
|
|
|
|
* @param workflowId - command option to force a specific workflow (mode id)
|
|
|
|
|
* instead of the default/first applicable one
|
|
|
|
|
*/
|
|
|
|
|
launchDefaultMode: ({ study, workflows = [], defaultWorkflow, workflowId }) => {
|
|
|
|
|
const workflow = workflowId
|
|
|
|
|
? workflows.find(w => w.id === workflowId)
|
|
|
|
|
: (defaultWorkflow ?? workflows[0]);
|
|
|
|
|
if (!workflow) {
|
|
|
|
|
console.warn(
|
|
|
|
|
workflowId
|
|
|
|
|
? `launchDefaultMode: workflow '${workflowId}' is not applicable to the study`
|
|
|
|
|
: 'launchDefaultMode: no workflow is applicable to the study'
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
workflow.launchWithStudy(study);
|
|
|
|
|
},
|
2023-03-15 17:41:41 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const definitions = {
|
2025-01-13 16:49:08 +01:00
|
|
|
multimonitor: actions.multimonitor,
|
2025-03-20 14:22:45 +01:00
|
|
|
promptSaveReport: actions.promptSaveReport,
|
2025-01-13 16:49:08 +01:00
|
|
|
loadStudy: actions.loadStudy,
|
2025-01-10 04:01:42 +01:00
|
|
|
showContextMenu: actions.showContextMenu,
|
|
|
|
|
closeContextMenu: actions.closeContextMenu,
|
|
|
|
|
clearMeasurements: actions.clearMeasurements,
|
|
|
|
|
displayNotification: actions.displayNotification,
|
|
|
|
|
setHangingProtocol: actions.setHangingProtocol,
|
|
|
|
|
toggleHangingProtocol: actions.toggleHangingProtocol,
|
|
|
|
|
navigateHistory: actions.navigateHistory,
|
2023-03-15 17:41:41 +01:00
|
|
|
nextStage: {
|
|
|
|
|
commandFn: actions.deltaStage,
|
|
|
|
|
options: { direction: 1 },
|
|
|
|
|
},
|
|
|
|
|
previousStage: {
|
|
|
|
|
commandFn: actions.deltaStage,
|
|
|
|
|
options: { direction: -1 },
|
|
|
|
|
},
|
2025-01-10 04:01:42 +01:00
|
|
|
setViewportGridLayout: actions.setViewportGridLayout,
|
|
|
|
|
toggleOneUp: actions.toggleOneUp,
|
|
|
|
|
openDICOMTagViewer: actions.openDICOMTagViewer,
|
|
|
|
|
updateViewportDisplaySet: actions.updateViewportDisplaySet,
|
2025-04-11 18:41:04 +02:00
|
|
|
scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView,
|
2025-05-05 22:35:05 +02:00
|
|
|
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
|
|
|
|
|
removeDisplaySetLayer: actions.removeDisplaySetLayer,
|
2026-03-17 13:59:47 +01:00
|
|
|
createStoreFunction: actions.createStoreFunction,
|
2026-07-10 14:16:16 +02:00
|
|
|
launchDefaultMode: {
|
|
|
|
|
commandFn: actions.launchDefaultMode,
|
|
|
|
|
context: 'WORKLIST',
|
|
|
|
|
},
|
2023-03-15 17:41:41 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
actions,
|
|
|
|
|
definitions,
|
|
|
|
|
defaultContext: 'DEFAULT',
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default commandsModule;
|