fix: ohif tests to run with cornerstone 3d 5.0 (#6043)

* chore(tests): Update multiple screenshot test images for various specs

* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts

* fix(DICOMSRDisplayTool): Improve actor presence check in viewport

* chore(tests): Update multiple screenshot assets for various specs

* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability

* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs

* test: update progressive loading screenshots

* jest 30 test fixes for compatibility with pnpm cs3d

* Use correct setDisplaySets instead of setDataId

* fix: Naming change for LegacyVolumeViewport3D

* Update to allow tolerance for contour tests

* update

* fix

* refactor: Replace instanceof checks with utility functions for viewport type validation

* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully

* bun lock

* fix: Install cs3d with pnpm instead of bun

* Update node version for playwright

* Update to v5.0.0 of cs3d

* fix: Build dependency

* audit

* Change to a web await retry assert

* Fix timing related test failures

* fix: Freehand close

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
AlirezaandBill Wallace authored and GitHub committed 2026-06-09 20:25:14 -04:00
1 parent a27f779f0b
commit 6dd150d401
172 files changed
+3081 -1866

No files matched your search

@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.22.10",
"@cornerstonejs/core": "4.22.10",
"@kitware/vtk.js": "34.15.1"
"@cornerstonejs/adapters": "5.0.0",
"@cornerstonejs/core": "5.0.0",
"@kitware/vtk.js": "35.5.3"
}
}
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.22.10",
"@cornerstonejs/core": "4.22.10",
"@kitware/vtk.js": "34.15.1"
"@cornerstonejs/adapters": "5.0.0",
"@cornerstonejs/core": "5.0.0",
"@kitware/vtk.js": "35.5.3"
}
}
+3 -3
View File
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.22.10",
"@cornerstonejs/core": "4.22.10",
"@cornerstonejs/tools": "4.22.10",
"@cornerstonejs/adapters": "5.0.0",
"@cornerstonejs/core": "5.0.0",
"@cornerstonejs/tools": "5.0.0",
"classnames": "2.5.1"
}
}
@@ -69,7 +69,12 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
);
if (!viewport._actors?.size) {
const hasActors =
typeof viewport.getActors === 'function'
? viewport.getActors().length > 0
: Boolean(viewport._actors?.size);
if (!hasActors) {
return;
}
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@cornerstonejs/core": "4.22.10",
"@cornerstonejs/tools": "4.22.10",
"@cornerstonejs/core": "5.0.0",
"@cornerstonejs/tools": "5.0.0",
"classnames": "2.5.1"
}
}
+9 -8
View File
@@ -40,7 +40,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.22.10",
"@cornerstonejs/dicom-image-loader": "5.0.0",
"@ohif/core": "3.13.0-beta.88",
"@ohif/ui": "3.13.0-beta.88",
"dcmjs": "0.49.4",
@@ -53,15 +53,16 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.22.10",
"@cornerstonejs/ai": "4.22.10",
"@cornerstonejs/core": "4.22.10",
"@cornerstonejs/labelmap-interpolation": "4.22.10",
"@cornerstonejs/polymorphic-segmentation": "4.22.10",
"@cornerstonejs/tools": "4.22.10",
"@cornerstonejs/adapters": "5.0.0",
"@cornerstonejs/ai": "5.0.0",
"@cornerstonejs/core": "5.0.0",
"@cornerstonejs/labelmap-interpolation": "5.0.0",
"@cornerstonejs/metadata": "5.0.0",
"@cornerstonejs/polymorphic-segmentation": "5.0.0",
"@cornerstonejs/tools": "5.0.0",
"@icr/polyseg-wasm": "0.4.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "34.15.1",
"@kitware/vtk.js": "35.5.3",
"html2canvas": "1.4.1",
"immutability-helper": "3.1.1",
"lodash.compact": "3.0.1",
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { Enums, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../utils/getLegacyViewportType';
function CornerstoneImageScrollbar({
viewportData,
@@ -40,7 +41,7 @@ function CornerstoneImageScrollbar({
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
@@ -69,7 +70,7 @@ function CornerstoneImageScrollbar({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { utilities as csUtils } from '@cornerstonejs/core';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import {
SmartScrollbar,
SmartScrollbarTrack,
@@ -102,7 +103,7 @@ function ViewportSliceProgressScrollbar({
const onScrollbarValueChange = targetImageIndex => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
@@ -1,5 +1,6 @@
import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/core';
import { ViewportData } from './types';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
export function getImageIndexFromEvent(event): number | undefined {
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
@@ -19,7 +20,7 @@ export function getViewportImageIds(viewportData: ViewportData): string[] {
}
export function isProgressFullMode(viewportData: ViewportData, viewport): boolean {
if (!viewportData || !viewport || viewport instanceof VolumeViewport3D) {
if (!viewportData || !viewport || isVolume3DViewportType(viewport)) {
return false;
}
@@ -4,9 +4,9 @@ import {
Enums,
eventTarget,
utilities,
VolumeViewport3D,
} from '@cornerstonejs/core';
import { useByteArray } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import { getImageIdFromCacheEvent, getImageIndexFromEvent, isProgressFullMode } from './helpers';
import { ImageSliceData, ViewportData } from './types';
@@ -98,7 +98,7 @@ export function useViewportSliceSync({
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport && !(viewport instanceof VolumeViewport3D)) {
if (viewport && !isVolume3DViewportType(viewport)) {
try {
const currentImageIndex = viewport.getCurrentImageIdIndex();
const currentNumberOfSlices = viewport.getNumberOfSlices();
@@ -120,7 +120,7 @@ export function useViewportSliceSync({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
+21 -15
View File
@@ -1,11 +1,7 @@
import {
getEnabledElement,
StackViewport,
VolumeViewport,
utilities as csUtils,
Enums as CoreEnums,
Types as CoreTypes,
BaseVolumeViewport,
getRenderingEngines,
} from '@cornerstonejs/core';
import {
@@ -38,6 +34,12 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u
import { getViewportEnabledElement } from './utils/getViewportEnabledElement';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import {
isStackViewportType,
isOrthographicViewportType,
isVolume3DViewportType,
isVolumeViewportType,
} from './utils/getLegacyViewportType';
import {
usePositionPresentationStore,
useSegmentationPresentationStore,
@@ -324,7 +326,7 @@ function commandsModule({
// Todo: check if PMAP modality should be handled such as SEG
displaySet.Modality !== 'SEG'
? SegmentationRepresentations.Contour
: viewport.type === CoreEnums.ViewportType.VOLUME_3D
: isVolume3DViewportType(viewport)
? SegmentationRepresentations.Surface
: SegmentationRepresentations.Labelmap;
@@ -910,7 +912,7 @@ function commandsModule({
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
const volumeId = actions.getVolumeIdForDisplaySet({
viewportId,
displaySetInstanceUID,
@@ -986,8 +988,12 @@ function commandsModule({
},
getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
// `instanceof BaseVolumeViewport` is false for GenericViewport compat adapters,
// so use the capability guard instead (cornerstone 5.x generic-viewport guide).
// A stack-mode adapter passes the guard but getAllVolumeIds() returns [], so the
// result is the same null as before on stacks.
if (csUtils.viewportSupportsVolumeId(viewport)) {
const volumeIds = (viewport as CoreTypes.IVolumeViewport).getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
return volumeId;
}
@@ -1256,7 +1262,7 @@ function commandsModule({
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
if (direction) {
const { parallelScale } = viewport.getCamera();
viewport.setCamera({ parallelScale: parallelScale * scaleFactor });
@@ -1286,9 +1292,9 @@ function commandsModule({
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
let numberOfSlices = 0;
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
numberOfSlices = viewport.getImageIds().length;
} else if (viewport instanceof VolumeViewport) {
} else if (isOrthographicViewportType(viewport)) {
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices;
} else {
throw new Error('Unsupported viewport type');
@@ -1344,11 +1350,11 @@ function commandsModule({
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
viewport.setProperties({ colormap });
}
if (viewport instanceof VolumeViewport) {
if (isOrthographicViewportType(viewport)) {
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
@@ -2145,7 +2151,7 @@ function commandsModule({
setViewportOrientation: ({ viewportId, orientation }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) {
if (!viewport || !isOrthographicViewportType(viewport)) {
console.warn('Orientation can only be set on volume viewports');
return;
}
@@ -2227,7 +2233,7 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
const camera = viewport.getCamera();
const rotAngle = (rotation * Math.PI) / 180;
const rotMat = mat4.identity(new Float32Array(16));
@@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useState, ReactElement, useMemo } from '
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { BaseVolumeViewport, Enums, eventTarget } from '@cornerstonejs/core';
import { Enums, eventTarget, utilities as csUtils, Types } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
getNodeOpacity,
@@ -27,10 +27,16 @@ const ViewportWindowLevel = ({
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports();
// getVolumeViewports() was removed in the GenericViewport architecture
// (a PLANAR_NEXT viewport can be volume-capable without being a VolumeViewport).
// Official replacement: getViewports() + the viewportSupportsVolumeCompatibility
// capability guard (cornerstone codemod cornerstone3d/5/generic-viewport).
const viewports = renderingEngine
.getViewports()
.filter(csUtils.viewportSupportsVolumeCompatibility);
return viewports.filter(vp => {
const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : [];
const viewportVolumeIds = (vp as Types.IVolumeViewport).getAllVolumeIds();
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@@ -124,8 +130,9 @@ const ViewportWindowLevel = ({
return;
}
const viewportVolumeIds =
viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : [];
const viewportVolumeIds = csUtils.viewportSupportsVolumeId(viewport)
? (viewport as Types.IVolumeViewport).getAllVolumeIds()
: [];
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useSystem } from '@ohif/core/src';
import { useViewportDisplaySets } from './useViewportDisplaySets';
import { BaseVolumeViewport } from '@cornerstonejs/core';
import { isVolumeViewportType } from '../utils/getLegacyViewportType';
/**
* Hook that provides measurement tracking information for a viewport
@@ -54,7 +54,7 @@ export function useMeasurementTracking({ viewportId }: { viewportId: string }) {
const viewport = cornerstoneViewportService?.getCornerstoneViewport(viewportId);
const SeriesInstanceUID = backgroundDisplaySet.SeriesInstanceUID;
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
const currentImageId = viewport?.getCurrentImageId();
if (!currentImageId) {
@@ -1,16 +1,13 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { useSystem } from '@ohif/core';
import { useViewportDisplaySets } from './useViewportDisplaySets';
import {
StackViewport,
Types,
VolumeViewport3D,
utilities,
Enums,
BaseVolumeViewport,
cache,
} from '@cornerstonejs/core';
import { Types, utilities, Enums, cache } from '@cornerstonejs/core';
import { getDataIdForViewport } from '../utils/getDataIdForViewport';
import {
isStackViewportType,
isVolumeViewportType,
isVolume3DViewportType,
} from '../utils/getLegacyViewportType';
import { WindowLevelPreset } from '../types/WindowLevel';
import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar';
import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig';
@@ -130,7 +127,7 @@ export function useViewportRendering(
const [viewport, setViewport] = useState<Types.IViewport | null>(() =>
viewportId ? (cornerstoneViewportService.getCornerstoneViewport(viewportId) ?? null) : null
);
const [is3DVolume, setIs3DVolume] = useState(viewport instanceof VolumeViewport3D);
const [is3DVolume, setIs3DVolume] = useState(isVolume3DViewportType(viewport));
const [opacity, setOpacityState] = useState<number | undefined>();
const [opacityLinear, setOpacityLinearState] = useState<number | undefined>();
const [threshold, setThresholdState] = useState<number | undefined>();
@@ -219,7 +216,7 @@ export function useViewportRendering(
return;
}
if (!(viewport instanceof BaseVolumeViewport)) {
if (!isVolumeViewportType(viewport)) {
return;
}
@@ -264,7 +261,7 @@ export function useViewportRendering(
}, [allWindowLevelPresets, activeDisplaySetInstanceUID]);
useEffect(() => {
setIs3DVolume(viewport instanceof VolumeViewport3D);
setIs3DVolume(isVolume3DViewportType(viewport));
if (!viewport || !activeDisplaySetInstanceUID) {
return;
@@ -573,7 +570,7 @@ export function useViewportRendering(
const setOpacity = useCallback(
(opacityValue: number) => {
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
if (!viewport || !isVolumeViewportType(viewport)) {
return;
}
@@ -624,7 +621,7 @@ export function useViewportRendering(
const setThreshold = useCallback(
(thresholdValue: number) => {
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
if (!viewport || !isVolumeViewportType(viewport)) {
return;
}
@@ -665,7 +662,7 @@ export function useViewportRendering(
return null;
}
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return (
+4
View File
@@ -74,6 +74,10 @@ export default async function init({
rendering: {
...cornerstone.getConfiguration().rendering,
strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport,
// Opt-in: route legacy viewport types through the new GenericViewport render
// paths while keeping the legacy public API via compatibility adapters.
// No-op on cornerstone builds that predate the GenericViewport architecture.
useGenericViewport: Boolean(appConfig.useGenericViewport),
},
});
@@ -10,6 +10,10 @@ import {
metaData,
} from '@cornerstonejs/core';
import { ViewportType } from '@cornerstonejs/core/enums';
import {
isVolume3DViewportType,
isVolumeViewportType,
} from '../../utils/getLegacyViewportType';
import {
Enums as csToolsEnums,
@@ -312,7 +316,7 @@ class SegmentationService extends PubSubService {
let isConverted = false;
const defaultRepresentationType: csToolsEnums.SegmentationRepresentations =
csViewport.type === ViewportType.VOLUME_3D ? SURFACE : LABELMAP;
isVolume3DViewportType(csViewport) ? SURFACE : LABELMAP;
let representationTypeToUse = type || defaultRepresentationType;
if (representationTypeToUse === LABELMAP) {
@@ -1591,8 +1595,7 @@ class SegmentationService extends PubSubService {
}
private determineViewportAndSegmentationType(csViewport, segmentation) {
const isVolumeViewport =
csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D;
const isVolumeViewport = isVolumeViewportType(csViewport);
const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP];
return { isVolumeViewport, isVolumeSegmentation };
}
@@ -1623,7 +1626,7 @@ class SegmentationService extends PubSubService {
}
private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) {
if (csViewport.type === ViewportType.VOLUME_3D) {
if (isVolume3DViewportType(csViewport)) {
return {
representationTypeToUse: SURFACE,
isConverted: false,
@@ -7,6 +7,7 @@ import {
} from '@cornerstonejs/tools';
import { isAnyDisplaySetCommon } from '../../utils/isAnyDisplaySetCommon';
import { isVolume3DViewportType } from '../../utils/getLegacyViewportType';
const { createSynchronizer } = SynchronizerManager;
const { SEGMENTATION_REPRESENTATION_MODIFIED } = Enums.Events;
@@ -89,7 +90,7 @@ const segmentationRepresentationModifiedCallback = async (
// Ensure the segmentation representation aligns with the target viewport type.
const type: Enums.SegmentationRepresentations =
viewport.type === CoreEnums.ViewportType.VOLUME_3D
isVolume3DViewportType(viewport)
? Enums.SegmentationRepresentations.Surface
: ((segmentationRepresentationType as Enums.SegmentationRepresentations) ??
Enums.SegmentationRepresentations.Labelmap);
@@ -3,16 +3,11 @@ import { PubSubService } from '@ohif/core';
import { Types as OhifTypes } from '@ohif/core';
import {
RenderingEngine,
StackViewport,
Types,
getRenderingEngine,
utilities as csUtils,
VolumeViewport,
VolumeViewport3D,
ECGViewport,
cache,
Enums as csEnums,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
@@ -39,6 +34,12 @@ import { usePositionPresentationStore } from '../../stores/usePositionPresentati
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import getClosestOrientationFromIOP from '../../utils/isReferenceViewable';
import {
getLegacyViewportType,
isStackViewportType,
isVolume3DViewportType,
isVolumeViewportType,
} from '../../utils/getLegacyViewportType';
import { BlendModes } from '@cornerstonejs/core/enums';
const EVENTS = {
@@ -49,10 +50,19 @@ const EVENTS = {
const MIN_STACK_VIEWPORTS_TO_ENQUEUE_RESIZE = 12;
const MIN_VOLUME_VIEWPORTS_TO_ENQUEUE_RESIZE = 6;
// Actor class names that represent a primary volume binding in a viewport.
// Legacy ORTHOGRAPHIC / VOLUME_3D render the source volume as a 'vtkVolume'
// actor. Under the GenericViewport-backed compat path (useGenericViewport),
// the MPR slice render path emits 'vtkImageSlice' actors instead, so the
// volume-set-vs-add optimization below must recognize both. (3D still uses
// 'vtkVolume'.) Without this, the optimization silently no-ops and a full
// setVolumes() teardown blanks MPR during SEG hydration.
const VOLUME_ACTOR_CLASS_NAMES = new Set(['vtkVolume', 'vtkImageSlice']);
function getVolumeActorReferencedIds(viewport: Types.IVolumeViewport): string[] {
const actors = viewport.getActors?.() ?? [];
return actors
.filter(ac => ac.actor?.getClassName?.() === 'vtkVolume')
.filter(ac => VOLUME_ACTOR_CLASS_NAMES.has(ac.actor?.getClassName?.() ?? ''))
.map(ac => ac.referencedId)
.filter(Boolean) as string[];
}
@@ -81,7 +91,7 @@ function viewportMatchesDesiredVolumePresentation(
desiredViewportInfo: ViewportInfo
): boolean {
const desiredType = desiredViewportInfo.getViewportType();
if (viewport.type !== desiredType) {
if (getLegacyViewportType(viewport) !== desiredType) {
return false;
}
return (
@@ -370,7 +380,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return {
viewportType: viewportInfo.getViewportType(),
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
viewReference: isVolume3DViewportType(csViewport) ? null : csViewport.getViewReference(),
viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewportId,
};
@@ -398,10 +408,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return properties;
};
const properties =
csViewport instanceof BaseVolumeViewport
? new Map()
: cleanProperties(csViewport.getProperties());
const properties = isVolumeViewportType(csViewport)
? new Map()
: cleanProperties(csViewport.getProperties());
if (properties instanceof Map) {
const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
@@ -840,10 +849,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
_presentations: Presentations = {}
): Promise<void> {
const [displaySet] = viewportData.data;
return viewport.setDataIds(displaySet.imageIds, {
groupId: displaySet.displaySetInstanceUID,
viewReference: viewportInfo.getViewReference(),
});
// CS3D's "redo viewports" replaced setDataIds with the generic
// setDisplaySets({ displaySetId }) API; the legacy adapters key off
// imageIds[0] as the displaySetId, so do the same here.
await viewport.setDisplaySets({ displaySetId: displaySet.imageIds[0] });
const viewReference = viewportInfo.getViewReference();
if (viewReference) {
viewport.setViewReference(viewReference);
}
}
private async _setStackViewport(
@@ -1141,7 +1154,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
throw new Error('Background display set not found');
}
if (viewport.type === csEnums.ViewportType.VOLUME_3D) {
if (isVolume3DViewportType(viewport)) {
timeoutViewportCallback(() => {
viewportGridService.setDisplaySetsForViewport({
viewportId: viewport.id,
@@ -1311,7 +1324,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
let displaySetPromise;
if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
if (isVolumeViewportType(viewport)) {
displaySetPromise = this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => {
if (keepCamera) {
viewport.setCamera(viewportCamera);
@@ -1320,7 +1333,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
displaySetPromise = this._setStackViewport(viewport, viewportData, viewportInfo);
}
@@ -1338,7 +1351,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportInfo: ViewportInfo,
presentations: Presentations = {}
): Promise<void> {
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
return this._setStackViewport(
viewport,
viewportData as StackViewportData,
@@ -1347,7 +1360,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
);
}
if ([VolumeViewport, VolumeViewport3D].some(type => viewport instanceof type)) {
if (isVolumeViewportType(viewport)) {
return this._setVolumeViewport(
viewport as Types.IVolumeViewport,
viewportData as VolumeViewportData,
@@ -1356,7 +1369,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
);
}
if (viewport instanceof ECGViewport) {
if (getLegacyViewportType(viewport) === csEnums.ViewportType.ECG) {
return this._setEcgViewport(
viewport as unknown as Types.IECGViewport,
viewportData as StackViewportData
@@ -1508,7 +1521,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
const { properties } = lutPresentation;
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
if (properties instanceof Map) {
properties.forEach((propertiesEntry, volumeId) => {
viewport.setProperties(propertiesEntry, volumeId);
@@ -1557,7 +1570,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { segmentationId, type, hydrated } = presentationItem;
const { Labelmap, Surface } = csToolsEnums.SegmentationRepresentations;
const isVolume3D = viewport.type === csEnums.ViewportType.VOLUME_3D;
const isVolume3D = isVolume3DViewportType(viewport);
// Determine the appropriate segmentation representation for the viewport.
// If the current type is Surface but the viewport is not 3D, fallback to Labelmap.
@@ -2,10 +2,10 @@ import {
Types,
Enums,
getEnabledElementByViewportId,
VolumeViewport,
utilities,
} from '@cornerstonejs/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { isOrthographicViewportType } from '../../utils/getLegacyViewportType';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
@@ -105,7 +105,7 @@ const dataContains = ({ data, displaySetUID, imageId, viewport }): boolean => {
return !!data.imageIds.find(dataId => dataId === imageId);
}
if (imageId && (data.volumeId || viewport instanceof VolumeViewport)) {
if (imageId && (data.volumeId || isOrthographicViewportType(viewport))) {
const isAcquisition = !!viewport.getCurrentImageId();
if (!isAcquisition) {
@@ -1,8 +1,9 @@
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
import { metaData, utilities } from '@cornerstonejs/core';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types';
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
import { guid, b64toBlob } from '@ohif/core/src/utils';
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
import { isOrthographicViewportType } from '../utils/getLegacyViewportType';
interface CachedStat {
color: number[]; // [r, g, b, a]
@@ -49,7 +50,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
onSetToolDisabled = (): void => {};
protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string {
if (viewport instanceof VolumeViewport) {
if (isOrthographicViewportType(viewport)) {
return;
}
@@ -1,9 +1,10 @@
import { utils } from '@ohif/core';
import React, { useEffect, useState } from 'react';
import html2canvas from 'html2canvas';
import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core';
import { getEnabledElement } from '@cornerstonejs/core';
import { ToolGroupManager, segmentation, Enums } from '@cornerstonejs/tools';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
import { isStackViewportType, isVolumeViewportType } from './getLegacyViewportType';
import { useSystem } from '@ohif/core/src';
const { downloadUrl } = utils;
@@ -127,10 +128,10 @@ const CornerstoneViewportDownloadForm = ({
const viewPresentation = viewport.getViewPresentation?.();
const viewRef = viewport.getViewReference?.();
if (downloadViewport instanceof StackViewport) {
if (isStackViewportType(downloadViewport)) {
const imageId = viewport.getCurrentImageId();
await downloadViewport.setStack([imageId]);
} else if (downloadViewport instanceof BaseVolumeViewport) {
} else if (isVolumeViewportType(downloadViewport)) {
const volumeIds = viewport.getAllVolumeIds();
await downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]);
}
@@ -36,7 +36,7 @@ type CreateSegmentationForViewportParams = {
export async function createSegmentationForViewport(
servicesManager: ServicesManager,
{ viewportId, options = {}, segmentationType }: CreateSegmentationForViewportParams
): Promise<string> {
): Promise<string | undefined> {
const { viewportGridService, displaySetService, segmentationService } = servicesManager.services;
const { viewports } = viewportGridService.getState();
const targetViewportId = viewportId;
@@ -44,7 +44,15 @@ export async function createSegmentationForViewport(
const viewport = viewports.get(targetViewportId);
// Todo: add support for multiple display sets
const displaySetInstanceUID = options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
// Guard against the grid not having this viewport yet (a transient race when
// "Add segmentation" runs before the viewport is registered in the grid). Bail
// gracefully instead of throwing, which would surface an uncaught error overlay.
const displaySetInstanceUID =
options.displaySetInstanceUID || viewport?.displaySetInstanceUIDs?.[0];
if (!displaySetInstanceUID) {
return undefined;
}
const segs = segmentationService.getSegmentations();
@@ -30,10 +30,14 @@ const mockDocument = {
},
};
Object.defineProperty(global, 'document', {
writable: true,
value: mockDocument,
});
// jsdom (jest 30) makes the global `document` non-configurable, so it can't be
// replaced wholesale. Route the methods the code under test uses onto the real
// jsdom document; assertions still target mockDocument's jest.fn() references.
const doc = document as unknown as typeof mockDocument;
doc.addEventListener = mockDocument.addEventListener;
doc.createElement = mockDocument.createElement;
document.body.appendChild = mockDocument.body.appendChild as typeof document.body.appendChild;
document.body.removeChild = mockDocument.body.removeChild as typeof document.body.removeChild;
describe('generateSegmentationCSVReport', () => {
const mockInfo = {
@@ -0,0 +1,66 @@
import { Enums as csEnums, Types as csTypes } from '@cornerstonejs/core';
const { ViewportType } = csEnums;
type ViewportLike = {
requestedType?: csEnums.ViewportType;
type?: csEnums.ViewportType;
};
/**
* Returns the legacy/requested viewport type, transparent across GenericViewport
* compatibility remapping.
*
* When `rendering.useGenericViewport` is enabled, a viewport requested as
* `STACK` or `ORTHOGRAPHIC` has runtime `viewport.type === PLANAR_NEXT`, and a
* `VOLUME_3D` request has `VOLUME_3D_NEXT`. The rendering engine records the
* original request on `viewport.requestedType`, which this reads. Falls back to
* `viewport.type` for legacy/non-remapped viewports (and older cornerstone builds
* that do not populate `requestedType`).
*
* Use this instead of `viewport instanceof StackViewport/VolumeViewport/...` or
* raw `viewport.type === ViewportType.X` when branching on the legacy viewport
* type. For "does this viewport support operation X" questions, prefer the
* cornerstone capability guards (`utilities.viewportSupports*`) instead.
*/
export function getLegacyViewportType(
viewport: unknown
): csEnums.ViewportType | undefined {
const vp = viewport as ViewportLike | null | undefined;
return vp?.requestedType ?? vp?.type;
}
/** Legacy STACK viewport (image stack). Replaces `instanceof StackViewport`. */
export function isStackViewportType(
viewport: unknown
): viewport is csTypes.IStackViewport {
return getLegacyViewportType(viewport) === ViewportType.STACK;
}
/** Legacy ORTHOGRAPHIC (MPR) viewport. Replaces `instanceof VolumeViewport`. */
export function isOrthographicViewportType(
viewport: unknown
): viewport is csTypes.IVolumeViewport {
return getLegacyViewportType(viewport) === ViewportType.ORTHOGRAPHIC;
}
/** Legacy VOLUME_3D viewport. Replaces `instanceof VolumeViewport3D`. */
export function isVolume3DViewportType(
viewport: unknown
): viewport is csTypes.IVolumeViewport {
return getLegacyViewportType(viewport) === ViewportType.VOLUME_3D;
}
/**
* Legacy ORTHOGRAPHIC or VOLUME_3D viewport (i.e. a `BaseVolumeViewport`).
* Replaces `instanceof BaseVolumeViewport`.
*/
export function isVolumeViewportType(
viewport: unknown
): viewport is csTypes.IVolumeViewport {
const legacyType = getLegacyViewportType(viewport);
return (
legacyType === ViewportType.ORTHOGRAPHIC ||
legacyType === ViewportType.VOLUME_3D
);
}
+2 -2
View File
@@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "4.22.10",
"@cornerstonejs/tools": "4.22.10",
"@cornerstonejs/core": "5.0.0",
"@cornerstonejs/tools": "5.0.0",
"@ohif/core": "3.13.0-beta.88",
"@ohif/extension-cornerstone-dicom-sr": "3.13.0-beta.88",
"@ohif/extension-default": "3.13.0-beta.88",
+1 -3
View File
@@ -362,9 +362,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
}
const referencedVolume =
csTools.utilities.segmentation.getReferenceVolumeForSegmentationVolume(
labelmapVolume.volumeId
);
csTools.utilities.segmentation.getReferenceVolumeForSegmentation(segmentationId);
if (!referencedVolume) {
report[id] = segReport;
+2 -2
View File
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "7.29.7",
"@cornerstonejs/core": "4.22.10",
"@cornerstonejs/tools": "4.22.10",
"@cornerstonejs/core": "5.0.0",
"@cornerstonejs/tools": "5.0.0",
"@ohif/core": "3.13.0-beta.88",
"@ohif/extension-cornerstone": "3.13.0-beta.88",
"@ohif/extension-default": "3.13.0-beta.88",