feat: add support to scoord3d (#5016)

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
Pedro Köhler 2025-07-31 01:21:26 -03:00 committed by GitHub
parent af40f06524
commit 735405a855
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 1032 additions and 993 deletions

3
.gitignore vendored
View File

@ -60,3 +60,6 @@ tests/playwright-report/
/playwright/.cache/ /playwright/.cache/
**/.claude/settings.local.json **/.claude/settings.local.json
# Backup files
*~

View File

@ -38,7 +38,7 @@
"resolutions": { "resolutions": {
"**/@babel/runtime": "^7.20.13", "**/@babel/runtime": "^7.20.13",
"commander": "8.3.0", "commander": "8.3.0",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicomweb-client": ">=0.10.4", "dicomweb-client": ">=0.10.4",
"nth-check": "^2.1.1", "nth-check": "^2.1.1",
"trim-newlines": "^5.0.0", "trim-newlines": "^5.0.0",

613
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -46,8 +46,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3", "@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3" "react-color": "^2.19.3"
} }

View File

@ -46,8 +46,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3", "@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3" "react-color": "^2.19.3"
} }

View File

@ -38,7 +38,7 @@
"@ohif/extension-cornerstone": "3.11.0-beta.114", "@ohif/extension-cornerstone": "3.11.0-beta.114",
"@ohif/extension-measurement-tracking": "3.11.0-beta.114", "@ohif/extension-measurement-tracking": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -46,9 +46,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3", "@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.30.3", "@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -32,12 +32,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options: Optio
additionalFindingTypes additionalFindingTypes
); );
const report = MeasurementReport.generateReport( const report = MeasurementReport.generateReport(filteredToolState, metaData, options);
filteredToolState,
metaData,
utilities.worldToImageCoords,
options
);
const { dataset } = report; const { dataset } = report;
@ -54,7 +49,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options: Optio
const commandsModule = (props: withAppTypes) => { const commandsModule = (props: withAppTypes) => {
const { servicesManager, extensionManager, commandsManager } = props; const { servicesManager, extensionManager, commandsManager } = props;
const { customizationService, viewportGridService, displaySetService } = servicesManager.services; const { customizationService } = servicesManager.services;
const actions = { const actions = {
changeColorMeasurement: ({ uid }) => { changeColorMeasurement: ({ uid }) => {
@ -129,6 +124,9 @@ const commandsModule = (props: withAppTypes) => {
console.log('naturalizedReport missing imaging content', naturalizedReport); console.log('naturalizedReport missing imaging content', naturalizedReport);
throw new Error('Invalid report, no content'); throw new Error('Invalid report, no content');
} }
if (!naturalizedReport.SOPClassUID) {
throw new Error('No sop class uid');
}
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore'); const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');

View File

@ -63,18 +63,12 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
const updateViewport = useCallback( const updateViewport = useCallback(
newMeasurementSelected => { newMeasurementSelected => {
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet; const { StudyInstanceUID, displaySetInstanceUID } = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) { if (!StudyInstanceUID || !displaySetInstanceUID) {
return; return;
} }
if (sopClassUids && sopClassUids.length > 1) {
// Todo: what happens if there are multiple SOP Classes? Why we are
// not throwing an error?
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
_getViewportReferencedDisplaySetData( _getViewportReferencedDisplaySetData(
srDisplaySet, srDisplaySet,
newMeasurementSelected, newMeasurementSelected,
@ -258,6 +252,9 @@ async function _getViewportReferencedDisplaySetData(
} }
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!referencedDisplaySet?.images) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const image0 = referencedDisplaySet.images[0]; const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = { const referencedDisplaySetMetadata = {

View File

@ -15,8 +15,12 @@ import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils; const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes; const { MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; const {
TEXT_ANNOTATION_POSITION,
COMMENT_CODE,
CodeScheme: Cornerstone3DCodeScheme,
} = adaptersSR.Cornerstone3D;
type InstanceMetadata = Types.InstanceMetadata; type InstanceMetadata = Types.InstanceMetadata;
@ -216,6 +220,12 @@ async function _load(
}); });
} }
function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
return (
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID
);
}
/** /**
* Checks if measurements can be added to a display set. * Checks if measurements can be added to a display set.
* *
@ -236,18 +246,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
measurement => measurement.loaded === false measurement => measurement.loaded === false
); );
if ( if (!unloadedMeasurements.length || newDisplaySet.unsupported) {
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
return; return;
} }
// const { sopClassUids } = newDisplaySet;
// Create a Set for faster lookups
// const sopClassUidSet = new Set(sopClassUids);
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number // Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
const imageIdMap = new Map<string, string>(); const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet); const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
@ -266,6 +268,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j]; let measurement = unloadedMeasurements[j];
const is3DMeasurement = measurement.coords?.[0]?.ValueType === 'SCOORD3D';
const onBeforeSRAddMeasurement = customizationService.getCustomization( const onBeforeSRAddMeasurement = customizationService.getCustomization(
'onBeforeSRAddMeasurement' 'onBeforeSRAddMeasurement'
@ -280,9 +283,15 @@ function _checkIfCanAddMeasurementsToDisplaySet(
} }
// if it is 3d SR we can just add the SR annotation // if it is 3d SR we can just add the SR annotation
if (is3DSR) { if (
is3DSR &&
is3DMeasurement &&
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
) {
addSRAnnotation(measurement, null, null); addSRAnnotation(measurement, null, null);
measurement.loaded = true; measurement.loaded = true;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
unloadedMeasurements.splice(j, 1);
continue; continue;
} }
@ -469,11 +478,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
* @returns {any} - The processed measurement result. * @returns {any} - The processed measurement result.
*/ */
function _processMeasurement(mergedContentSequence) { function _processMeasurement(mergedContentSequence) {
if ( if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) {
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
)
) {
return _processTID1410Measurement(mergedContentSequence); return _processTID1410Measurement(mergedContentSequence);
} }
@ -511,12 +516,25 @@ function _processTID1410Measurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const { ConceptNameCodeSequence: conceptNameItem } = graphicItem;
const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem;
const graphicCode = `${graphicDesignator}:${graphicValue}`;
const pointDataItem = _getCoordsFromSCOORDOrSCOORD3D(graphicItem);
const is3DMeasurement = pointDataItem.ValueType === 'SCOORD3D';
const pointLength = is3DMeasurement ? 3 : 2;
const pointsLength = pointDataItem.GraphicData.length / pointLength;
const measurement = { const measurement = {
loaded: false, loaded: false,
labels: [], labels: [],
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)], coords: [pointDataItem],
TrackingUniqueIdentifier: UIDREFContentItem.UID, TrackingUniqueIdentifier: UIDREFContentItem.UID,
TrackingIdentifier: TrackingIdentifierContentItem.TextValue, TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
graphicCode,
is3DMeasurement,
pointsLength,
graphicType: pointDataItem.GraphicType,
}; };
NUMContentItems.forEach(item => { NUMContentItems.forEach(item => {
@ -567,6 +585,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
); );
const commentSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === COMMENT_CODE.schemeDesignator &&
item.ConceptNameCodeSequence.CodeValue === COMMENT_CODE.value
);
const measurement = { const measurement = {
loaded: false, loaded: false,
labels: [], labels: [],
@ -575,6 +599,14 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
TrackingIdentifier: TrackingIdentifierContentItem.TextValue, TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
}; };
if (commentSites) {
for (const group of commentSites) {
if (group.TextValue) {
measurement.labels.push({ label: group.TextValue, value: '' });
}
}
}
if ( if (
finding && finding &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes( CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
@ -726,4 +758,17 @@ function _getSequenceAsArray(sequence) {
return Array.isArray(sequence) ? sequence : [sequence]; return Array.isArray(sequence) ? sequence : [sequence];
} }
function isScoordOr3d(group) {
return group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D';
}
function isTextPosition(group) {
const concept = group.ConceptNameCodeSequence[0];
return (
concept &&
concept.CodeValue === TEXT_ANNOTATION_POSITION.value &&
concept.CodingSchemeDesignator === TEXT_ANNOTATION_POSITION.schemeDesignator
);
}
export default getSopClassHandlerModule; export default getSopClassHandlerModule;

View File

@ -12,7 +12,6 @@ import {
utilities as csToolsUtils, utilities as csToolsUtils,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core'; import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool'; import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
@ -57,7 +56,7 @@ export default function init({
'SRSCOORD3DPoint', 'SRSCOORD3DPoint',
POINT, POINT,
SRSCOOR3DProbeMapper.toAnnotation, SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement SRSCOOR3DProbeMapper.toMeasurement.bind(null, { servicesManager })
); );
// Modify annotation tools to use dashed lines on SR // Modify annotation tools to use dashed lines on SR

View File

@ -1,4 +1,4 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; import { type Types } from '@cornerstonejs/core';
import { import {
annotation, annotation,
drawing, drawing,

View File

@ -7,13 +7,8 @@ const SRSCOOR3DProbe = {
* @param {Object} cornerstone Cornerstone event data * @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance * @return {Measurement} Measurement instance
*/ */
toMeasurement: ( toMeasurement: ({ servicesManager, getValueTypeFromToolType }, csToolsEventDetail) => {
csToolsEventDetail, const { displaySetService } = servicesManager.services;
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail; const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation; const { metadata, data, annotationUID } = annotation;
@ -22,14 +17,25 @@ const SRSCOOR3DProbe = {
return null; return null;
} }
const { toolName } = metadata; const { toolName, FrameOfReferenceUID } = metadata;
const { points } = data.handles; const { points } = data.handles;
const displaySets = displaySetService
.getActiveDisplaySets()
.filter(ds => ds.FrameOfReferenceUID === FrameOfReferenceUID);
const displaySet = displaySets.filter(ds => ds.isReconstructable)[0] || displaySets[0];
const { StudyInstanceUID: referenceStudyUID, SeriesInstanceUID: referenceSeriesUID } =
displaySets[0] || {};
const displayText = getDisplayText(annotation); const displayText = getDisplayText(annotation);
return { return {
uid: annotationUID, uid: annotationUID,
points, points,
metadata, metadata,
referenceStudyUID,
referenceSeriesUID,
displaySetInstanceUID: displaySet?.displaySetInstanceUID,
toolName: metadata.toolName, toolName: metadata.toolName,
label: data.label, label: data.label,
displayText: displayText, displayText: displayText,
@ -56,7 +62,10 @@ function getDisplayText(annotation) {
} }
} }
return displayText; return {
primary: displayText,
secondary: [],
};
} }
export default SRSCOOR3DProbe; export default SRSCOOR3DProbe;

View File

@ -1,9 +1,12 @@
import { Types, annotation } from '@cornerstonejs/tools'; import { Types, annotation } from '@cornerstonejs/tools';
import { metaData } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core';
import { adaptersSR } from '@cornerstonejs/adapters';
import getRenderableData from './getRenderableData'; import getRenderableData from './getRenderableData';
import toolNames from '../tools/toolNames'; import toolNames from '../tools/toolNames';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
export default function addSRAnnotation(measurement, imageId, frameNumber) { export default function addSRAnnotation(measurement, imageId, frameNumber) {
let toolName = toolNames.DICOMSRDisplay; let toolName = toolNames.DICOMSRDisplay;
const renderableData = measurement.coords.reduce((acc, coordProps) => { const renderableData = measurement.coords.reduce((acc, coordProps) => {
@ -25,7 +28,12 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
} }
if (valueType === 'SCOORD3D') { if (valueType === 'SCOORD3D') {
toolName = toolNames.SRSCOORD3DPoint; const adapter = MeasurementReport.getAdapterForTrackingIdentifier(
measurement.TrackingIdentifier
);
if (!adapter) {
toolName = toolNames.SRSCOORD3DPoint;
}
// get the ReferencedFrameOfReferenceUID from the measurement // get the ReferencedFrameOfReferenceUID from the measurement
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence; frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;

View File

@ -1,17 +1,11 @@
import OHIF from '@ohif/core';
import { annotation } from '@cornerstonejs/tools'; import { annotation } from '@cornerstonejs/tools';
const { log } = OHIF; import { NO_IMAGE_ID } from '@cornerstonejs/adapters';
function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) { function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
const filteredToolState = {}; const filteredToolState = {};
function addToFilteredToolState(annotation, toolType) { function addToFilteredToolState(annotation, toolType) {
if (!annotation.metadata?.referencedImageId) { const imageId = annotation.metadata?.referencedImageId ?? NO_IMAGE_ID;
log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
return;
}
const imageId = annotation.metadata.referencedImageId;
if (!filteredToolState[imageId]) { if (!filteredToolState[imageId]) {
filteredToolState[imageId] = {}; filteredToolState[imageId] = {};

View File

@ -11,7 +11,11 @@ const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
* @returns {string} The extracted label. * @returns {string} The extracted label.
*/ */
export default function getLabelFromDCMJSImportedToolData(toolData) { export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData; const { findingSites = [], finding, annotation } = toolData;
if (annotation.data.label) {
return annotation.data.label;
}
let freeTextLabel = findingSites.find( let freeTextLabel = findingSites.find(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT

View File

@ -36,6 +36,14 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
ValueType, ValueType,
imageId, imageId,
}); });
if (!imageId) {
// without the image id it's not possible to perform the calculations below
// these calculations also do not seem to be needed, since everything works
// just fine when we skip them. At least for SCOORD3D annotations.
return pointsWorld;
}
// We do not have an explicit draw circle svg helper in Cornerstone3D at // We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so // this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose. // here we reshape the data for that purpose.
@ -90,6 +98,13 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
imageId, imageId,
}); });
if (!imageId) {
// without the image id it's not possible to perform the calculations below
// these calculations also do not seem to be needed, since everything works
// just fine when we skip them. At least for SCOORD3D annotations.
return pointsWorld;
}
const majorAxisStart = vec3.fromValues(...pointsWorld[0]); const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
const minorAxisStart = vec3.fromValues(...pointsWorld[2]); const minorAxisStart = vec3.fromValues(...pointsWorld[2]);

View File

@ -1,15 +1,16 @@
import { utilities, metaData } from '@cornerstonejs/core'; import { utilities, metaData, type Types } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core'; import OHIF, { DicomMetadataStore } from '@ohif/core';
import { vec3 } from 'gl-matrix';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData'; import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters'; import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools'; import { annotation as CsAnnotation, type Types as ToolTypes } from '@cornerstonejs/tools';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
const { locking } = CsAnnotation; const { locking } = CsAnnotation;
const { guid } = OHIF.utils; const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D; const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => { const convertCode = (codingValues, code) => {
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') { if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
@ -37,8 +38,10 @@ const convertSites = (codingValues, sites) => {
}; };
/** /**
* Hydrates a structured report, for default viewports. * Hydrates a structured report
* * Handles 2d and 3d hydration from SCOORD and SCOORD3D points
* For 3D hydration, chooses a volume display set to display with
* FOr 2D hydration, chooses the (first) display set containing the referenced image.
*/ */
export default function hydrateStructuredReport( export default function hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager }: withAppTypes, { servicesManager, extensionManager, commandsManager }: withAppTypes,
@ -88,7 +91,7 @@ export default function hydrateStructuredReport(
// Mapping of legacy datasets is now directly handled by adapters module // Mapping of legacy datasets is now directly handled by adapters module
const datasetToUse = instance; const datasetToUse = instance;
// Use dcmjs to generate toolState. // Use CS3D adapters to generate toolState.
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState( let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse, datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them // NOTE: we need to pass in the imageIds to dcmjs since the we use them
@ -96,7 +99,6 @@ export default function hydrateStructuredReport(
// that measurements were added to the display set are the same order as // that measurements were added to the display set are the same order as
// the measurementGroups in the instance. // the measurementGroups in the instance.
sopInstanceUIDToImageId, sopInstanceUIDToImageId,
utilities.imageToWorldCoords,
metaData metaData
); );
@ -147,6 +149,9 @@ export default function hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) { for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i]; const imageId = imageIds[i];
if (!imageId) {
continue;
}
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId); const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
@ -160,38 +165,55 @@ export default function hydrateStructuredReport(
} }
} }
/**
* Gets reference data for what frame of reference and the referenced
* image id, or for 3d measurements, the volumeId to apply this annotation to.
*/
function getReferenceData(toolData): ToolTypes.AnnotationMetadata {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
if (!imageId) {
return getReferenceData3D(toolData, servicesManager);
}
const instance = metaData.get('instance', imageId);
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
return {
referencedImageId: imageId,
FrameOfReferenceUID,
};
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => { toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
toolData.uid = guid(); toolData.uid = guid();
const referenceData = getReferenceData(toolData);
const instance = metaData.get('instance', imageId); const { imageId } = referenceData;
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
const annotation = { const annotation = {
annotationUID: toolData.annotation.annotationUID, annotationUID: toolData.annotation.annotationUID,
data: toolData.annotation.data, data: toolData.annotation.data,
metadata: { metadata: {
...referenceData,
toolName: annotationType, toolName: annotationType,
referencedImageId: imageId,
FrameOfReferenceUID,
}, },
}; };
utilities.updatePlaneRestriction(annotation.data.handles.points, annotation.metadata);
const source = measurementService.getSource( const source = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_NAME,
@ -225,7 +247,7 @@ export default function hydrateStructuredReport(
locking.setAnnotationLocked(newAnnotationUID, true); locking.setAnnotationLocked(newAnnotationUID, true);
} }
if (!imageIds.includes(imageId)) { if (imageId && !imageIds.includes(imageId)) {
imageIds.push(imageId); imageIds.push(imageId);
} }
}); });
@ -238,3 +260,92 @@ export default function hydrateStructuredReport(
SeriesInstanceUIDs, SeriesInstanceUIDs,
}; };
} }
/**
* For 3d annotations, there are often several display sets which could
* be used to display the annotation. Choose the first annotation with the
* same frame of reference that is reconstructable, or the first display set
* otherwise.
*/
function chooseDisplaySet(displaySets, annotation) {
if (!displaySets?.length) {
console.warn('No display set found for', annotation);
return;
}
if (displaySets.length === 1) {
return displaySets[0];
}
const volumeDs = displaySets.find(ds => ds.isReconstructable);
if (volumeDs) {
return volumeDs;
}
return displaySets[0];
}
/**
* Gets the additional reference data appropriate for a 3d reference.
* This will choose a volume id, frame of reference and a plane restriction.
*/
function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) {
const { FrameOfReferenceUID } = toolData.annotation.metadata;
const { points } = toolData.annotation.data.handles;
const { displaySetService } = servicesManager.services;
const displaySetsFOR = displaySetService.getDisplaySetsBy(
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID
);
if (!displaySetsFOR.length || !points?.length) {
return {
FrameOfReferenceUID,
};
}
const ds = chooseDisplaySet(displaySetsFOR, toolData.annotation);
const cameraView = chooseCameraView(ds, points);
const viewReference = {
...cameraView,
volumeId: ds.displaySetInstanceUID,
FrameOfReferenceUID,
};
utilities.updatePlaneRestriction(points, viewReference);
return viewReference;
}
/**
* Chooses a possible camera view - right now this is fairly basic,
* just setting the unknowns to null.
*/
function chooseCameraView(_ds, points) {
const selectedPoints = choosePoints(points);
const cameraFocalPoint = <Point3>centerOf(selectedPoints);
// These are sufficient to be null for now and can be set on first view
let viewPlaneNormal: Types.Point3 = null;
let viewUp: Types.Point3 = null;
return {
cameraFocalPoint,
viewPlaneNormal,
viewUp,
};
}
function centerOf(points) {
const scale = 1 / points.length;
const center = vec3.create();
for (const point of points) {
vec3.scaleAndAdd(center, center, point, scale);
}
return center;
}
function choosePoints(points) {
if (points.length === 1 || points.length === 2) {
return points;
}
const firstIndex = 0;
const secondIndex = Math.ceil(points.length / 4);
const thirdIndex = Math.ceil(points.length / 2);
// TODO - check if colinear, if so try to find another 3 points.
const newPoints = [points[firstIndex], points[secondIndex], points[thirdIndex]];
return newPoints;
}

View File

@ -22,13 +22,20 @@ export default function isRehydratable(displaySet, mappings) {
const { measurements } = displaySet; const { measurements } = displaySet;
for (let i = 0; i < measurements.length; i++) { for (let i = 0; i < measurements.length; i++) {
const { TrackingIdentifier } = measurements[i] || {}; const measurement = measurements[i];
if (!TrackingIdentifier) { if (!measurement) {
console.warn('No tracking identifier for measurement ', measurements[i]); continue;
}
const { TrackingIdentifier = '', graphicType, graphicCode, pointsLength } = measurement;
if (!TrackingIdentifier && !graphicType) {
console.warn('No tracking identifier or graphicType for measurement ', measurement);
continue; continue;
} }
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(TrackingIdentifier); const adapter = MeasurementReport.getAdapterForTrackingIdentifier(TrackingIdentifier);
const hydratable = adapter && mappingDefinitions.has(adapter.toolType); const adapters = MeasurementReport.getAdaptersForTypes(graphicCode, graphicType, pointsLength);
const hydratable =
(adapter && mappingDefinitions.has(adapter.toolType)) ||
(adapters && adapters.some(adapter => mappingDefinitions.has(adapter.toolType)));
if (hydratable) { if (hydratable) {
return true; return true;

View File

@ -34,7 +34,7 @@
"@ohif/extension-default": "3.11.0-beta.114", "@ohif/extension-default": "3.11.0-beta.114",
"@ohif/i18n": "3.11.0-beta.114", "@ohif/i18n": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -42,8 +42,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.30.3", "@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -38,10 +38,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.30.3", "@cornerstonejs/dicom-image-loader": "^3.32.5",
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -51,12 +51,12 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3", "@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/ai": "^3.30.3", "@cornerstonejs/ai": "^3.32.5",
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/labelmap-interpolation": "^3.30.3", "@cornerstonejs/labelmap-interpolation": "^3.32.5",
"@cornerstonejs/polymorphic-segmentation": "^3.30.3", "@cornerstonejs/polymorphic-segmentation": "^3.32.5",
"@cornerstonejs/tools": "^3.30.3", "@cornerstonejs/tools": "^3.32.5",
"@itk-wasm/morphological-contour-interpolation": "1.1.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",

View File

@ -13,7 +13,6 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core'; import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners'; import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import ViewportColorbarsContainer from '../components/ViewportColorbar';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations'; import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore'; import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior'; import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
@ -305,31 +304,6 @@ const OHIFCornerstoneViewport = React.memo(
loadViewportData(); loadViewportData();
}, [viewportOptions, displaySets, dataSource]); }, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
event => handleJumpToMeasurement(event, elementRef, viewportId, cornerstoneViewportService)
);
return () => {
unsubscribe();
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
const Notification = customizationService.getCustomization('ui.notificationComponent'); const Notification = customizationService.getCustomization('ui.notificationComponent');
return ( return (
@ -387,54 +361,6 @@ const OHIFCornerstoneViewport = React.memo(
areEqual areEqual
); );
// Helper function to handle jumping to measurements
function handleJumpToMeasurement(event, elementRef, viewportId, cornerstoneViewportService) {
const { measurement, isConsumed } = event;
if (!measurement || isConsumed) {
return;
}
const enabledElement = getEnabledElement(elementRef.current);
if (!enabledElement) {
return;
}
const viewport = enabledElement.viewport as csTypes.IStackViewport | csTypes.IVolumeViewport;
const { metadata, displaySetInstanceUID } = measurement;
const viewportDisplaySets = cornerstoneViewportService.getViewportDisplaySets(viewportId);
const showingDisplaySet = viewportDisplaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
let metadataToUse = metadata;
// if it is not showing the displaySet we need to remove the FOR from the metadata
if (!showingDisplaySet) {
metadataToUse = {
...metadata,
FrameOfReferenceUID: undefined,
};
}
// Todo: make it work with cases where we want to define FOR based measurements too
if (!viewport.isReferenceViewable(metadataToUse, WITH_NAVIGATION)) {
return;
}
try {
viewport.setViewReference(metadata);
viewport.render();
} catch (e) {
console.warn('Unable to apply', metadata, e);
}
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
event?.consume?.();
}
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) { function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
const { synchronizersStore } = useSynchronizersStore.getState(); const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId]; const synchronizers = synchronizersStore[viewportId];
@ -558,16 +484,4 @@ function areEqual(prevProps, nextProps) {
return true; return true;
} }
// Helper function to check if display sets have changed
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
if (prevDisplaySets.length !== currentDisplaySets.length) {
return true;
}
return currentDisplaySets.some((currentDS, index) => {
const prevDS = prevDisplaySets[index];
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
});
}
export default OHIFCornerstoneViewport; export default OHIFCornerstoneViewport;

View File

@ -146,6 +146,70 @@ function commandsModule({
} }
const actions = { const actions = {
jumpToMeasurementViewport: ({ annotationUID, measurement }) => {
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
const { metadata } = measurement;
const activeViewportId = viewportGridService.getActiveViewportId();
// Finds the best viewport to jump to for showing the annotation view reference
// This may be different from active if there is a viewport already showing the display set.
const viewportId = cornerstoneViewportService.findNavigationCompatibleViewportId(
activeViewportId,
metadata
);
if (viewportId) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewport.setViewReference(metadata);
viewport.render();
return;
}
const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement;
if (!referencedDisplaySetInstanceUID) {
console.warn('ViewportGrid::No display set found in', measurement);
return;
}
// Finds the viewport to update to show the given displayset/orientation.
// This will choose a view already containing the measurement display set
// if possible, otherwise will fallback to the active.
const viewportToUpdate = cornerstoneViewportService.findUpdateableViewportConfiguration(
activeViewportId,
measurement
);
if (!viewportToUpdate) {
console.warn('Unable to find a viewport to show this in');
return;
}
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportToUpdate.viewportId,
referencedDisplaySetInstanceUID
);
if (!updatedViewports?.[0]) {
console.warn(
'ViewportGrid::Unable to navigate to viewport containing',
referencedDisplaySetInstanceUID
);
return;
}
updatedViewports[0].viewportOptions = viewportToUpdate.viewportOptions;
// Update stored position presentation
commandsManager.run('updateStoredPositionPresentation', {
viewportId: viewportToUpdate.viewportId,
displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
referencedImageId: measurement.referencedImageId,
options: {
...measurement.metadata,
},
});
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
},
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => { hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => {
if (!displaySet) { if (!displaySet) {
return; return;
@ -328,6 +392,8 @@ function commandsModule({
type, type,
}); });
}, },
/** Stores the changed position presentation */
updateStoredPositionPresentation: ({ updateStoredPositionPresentation: ({
viewportId, viewportId,
displaySetInstanceUIDs, displaySetInstanceUIDs,
@ -351,22 +417,23 @@ function commandsModule({
([key, value]) => { ([key, value]) => {
return ( return (
displaySetInstanceUIDs.every(uid => key.includes(uid)) && displaySetInstanceUIDs.every(uid => key.includes(uid)) &&
value.viewportId === viewportId value?.viewportId === viewportId
); );
} }
)?.[0]; )?.[0];
} }
// Create presentation data with referencedImageId and options if provided // Create presentation data with referencedImageId and options if provided
const presentationData = referencedImageId const presentationData =
? { referencedImageId || options?.FrameOfReferenceUID
...presentations.positionPresentation, ? {
viewReference: { ...presentations.positionPresentation,
referencedImageId, viewReference: {
...options, referencedImageId,
}, ...options,
} },
: presentations.positionPresentation; }
: presentations.positionPresentation;
if (previousReferencedDisplaySetStoreKey) { if (previousReferencedDisplaySetStoreKey) {
setPositionPresentation(previousReferencedDisplaySetStoreKey, presentationData); setPositionPresentation(previousReferencedDisplaySetStoreKey, presentationData);
@ -2153,9 +2220,7 @@ function commandsModule({
updateMeasurement: { updateMeasurement: {
commandFn: actions.updateMeasurement, commandFn: actions.updateMeasurement,
}, },
jumpToMeasurement: { jumpToMeasurement: actions.jumpToMeasurement,
commandFn: actions.jumpToMeasurement,
},
removeMeasurement: { removeMeasurement: {
commandFn: actions.removeMeasurement, commandFn: actions.removeMeasurement,
}, },
@ -2415,6 +2480,7 @@ function commandsModule({
startRecordingForAnnotationGroup: actions.startRecordingForAnnotationGroup, startRecordingForAnnotationGroup: actions.startRecordingForAnnotationGroup,
endRecordingForAnnotationGroup: actions.endRecordingForAnnotationGroup, endRecordingForAnnotationGroup: actions.endRecordingForAnnotationGroup,
toggleSegmentLabel: actions.toggleSegmentLabel, toggleSegmentLabel: actions.toggleSegmentLabel,
jumpToMeasurementViewport: actions.jumpToMeasurementViewport,
initializeSegmentLabelTool: actions.initializeSegmentLabelTool, initializeSegmentLabelTool: actions.initializeSegmentLabelTool,
}; };

View File

@ -24,36 +24,38 @@ export const groupByStudy = (items, grouping, childProps) => {
let firstSelected, firstGroup; let firstSelected, firstGroup;
items.forEach(item => { items
const studyUID = getItemStudyInstanceUID(item); .filter(item => item.displaySetInstanceUID)
if (!groups.has(studyUID)) { .forEach(item => {
const items = []; const studyUID = getItemStudyInstanceUID(item);
const filter = MeasurementFilters.filterAnd( if (!groups.has(studyUID)) {
MeasurementFilters.filterMeasurementsByStudyUID(studyUID), const items = [];
grouping.filter const filter = MeasurementFilters.filterAnd(
); MeasurementFilters.filterMeasurementsByStudyUID(studyUID),
const group = { grouping.filter
...grouping, );
items, const group = {
displayMeasurements: items, ...grouping,
key: studyUID, items,
isSelected: studyUID === activeStudyUID, displayMeasurements: items,
StudyInstanceUID: studyUID, key: studyUID,
filter, isSelected: studyUID === activeStudyUID,
measurementFilter: filter, StudyInstanceUID: studyUID,
}; filter,
if (group.isSelected && !firstSelected) { measurementFilter: filter,
firstSelected = group; };
if (group.isSelected && !firstSelected) {
firstSelected = group;
}
firstGroup ||= group;
groups.set(studyUID, group);
} }
firstGroup ||= group; if (!firstSelected && firstGroup) {
groups.set(studyUID, group); firstGroup.isSelected = true;
} }
if (!firstSelected && firstGroup) { const group = groups.get(studyUID);
firstGroup.isSelected = true; group.items.push(item);
} });
const group = groups.get(studyUID);
group.items.push(item);
});
return groups; return groups;
}; };

View File

@ -34,15 +34,20 @@ export default {
if (!viewportId) { if (!viewportId) {
return []; return [];
} }
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetInsaneUIDs) { if (!displaySetInstanceUIDs) {
return []; return [];
} }
const displaySets = displaySetInsaneUIDs.map(uid => const displaySets = displaySetInstanceUIDs.map(uid => {
displaySetService.getDisplaySetByUID(uid) const displaySet = displaySetService.getDisplaySetByUID(uid);
); const referencedDisplaySetUID = displaySet?.measurements?.[0]?.displaySetInstanceUID;
if (displaySet.Modality === 'SR' && referencedDisplaySetUID) {
return displaySetService.getDisplaySetByUID(referencedDisplaySetUID);
}
return displaySet;
});
return hangingProtocols return hangingProtocols
.map(hp => { .map(hp => {

View File

@ -279,7 +279,7 @@ export function useViewportRendering(
} }
// Get threshold from colormap if available // Get threshold from colormap if available
if (properties.colormap && properties.colormap.threshold !== undefined) { if (properties?.colormap && properties.colormap.threshold !== undefined) {
setThresholdState(properties.colormap.threshold); setThresholdState(properties.colormap.threshold);
} }
} }

View File

@ -195,7 +195,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
{ {
name: 'cornerstone', name: 'cornerstone',
component: ExtendedOHIFCornerstoneViewport, component: ExtendedOHIFCornerstoneViewport,
isReferenceViewable: props => utils.isReferenceViewable({ ...props, servicesManager }), isReferenceViewable: utils.isReferenceViewable.bind(null, servicesManager),
}, },
]; ];
}, },

View File

@ -189,17 +189,13 @@ export default async function init({
initCineService(servicesManager); initCineService(servicesManager);
initStudyPrefetcherService(servicesManager); initStudyPrefetcherService(servicesManager);
[ measurementService.subscribe(measurementService.EVENTS.JUMP_TO_MEASUREMENT, evt => {
measurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, const { measurement } = evt;
measurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, const { uid: annotationUID } = measurement;
].forEach(event => { commandsManager.runCommand('jumpToMeasurementViewport', { measurement, annotationUID, evt });
measurementService.subscribe(event, evt => {
const { measurement } = evt;
const { uid: annotationUID } = measurement;
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
});
}); });
// When a custom image load is performed, update the relevant viewports // When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe( hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,

View File

@ -436,7 +436,7 @@ const connectMeasurementServiceToTools = ({
return; return;
} }
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID } = measurement; const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID, metadata } = measurement;
const instance = DicomMetadataStore.getInstance( const instance = DicomMetadataStore.getInstance(
referenceStudyUID, referenceStudyUID,
@ -463,8 +463,12 @@ const connectMeasurementServiceToTools = ({
annotationUID: measurement.uid, annotationUID: measurement.uid,
highlighted: false, highlighted: false,
isLocked: false, isLocked: false,
invalidated: false, // This is used to force a re-render of the annotation to
// re-calculate cached stats since sometimes in SR we
// get empty cached stats
invalidated: true,
metadata: { metadata: {
...metadata,
toolName: measurement.toolName, toolName: measurement.toolName,
FrameOfReferenceUID: measurement.FrameOfReferenceUID, FrameOfReferenceUID: measurement.FrameOfReferenceUID,
referencedImageId: imageId, referencedImageId: imageId,

View File

@ -36,7 +36,6 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore'; import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore'; import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import { VOLUME_LOADER_SCHEME } from '../../constants';
const EVENTS = { const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged', VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@ -47,6 +46,7 @@ const MIN_STACK_VIEWPORTS_TO_ENQUEUE_RESIZE = 12;
const MIN_VOLUME_VIEWPORTS_TO_ENQUEUE_RESIZE = 6; const MIN_VOLUME_VIEWPORTS_TO_ENQUEUE_RESIZE = 6;
export const WITH_NAVIGATION = { withNavigation: true, withOrientation: false }; export const WITH_NAVIGATION = { withNavigation: true, withOrientation: false };
export const WITH_ORIENTATION = { withNavigation: true, withOrientation: true };
/** /**
* Handles cornerstone viewport logic including enabling, disabling, and * Handles cornerstone viewport logic including enabling, disabling, and
@ -551,10 +551,13 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
* was initiated. * was initiated.
* @return the viewportId that the measurement should be displayed in. * @return the viewportId that the measurement should be displayed in.
*/ */
public getViewportIdToJump(activeViewportId: string, metadata): string { public findNavigationCompatibleViewportId(activeViewportId: string, metadata): string {
// First check if the active viewport can just be navigated to show the given item // First check if the active viewport can just be navigated to show the given item
const activeViewport = this.getCornerstoneViewport(activeViewportId); const activeViewport = this.getCornerstoneViewport(activeViewportId);
if (activeViewport.isReferenceViewable(metadata, { withNavigation: true })) { if (!activeViewport) {
console.warn('No active viewport found for', activeViewportId);
}
if (activeViewport?.isReferenceViewable(metadata, { withNavigation: true })) {
return activeViewportId; return activeViewportId;
} }
@ -570,7 +573,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// No viewport is in the right display set/orientation to show this, so see if // No viewport is in the right display set/orientation to show this, so see if
// the active viewport could change orientations to show this // the active viewport could change orientations to show this
if ( if (
activeViewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true }) activeViewport?.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
) { ) {
return activeViewportId; return activeViewportId;
} }
@ -589,6 +592,86 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return null; return null;
} }
/**
* Figures out which viewport to update when the viewport type needs to change.
* This may not be the active viewport if there is already a viewport showing
* the display set, but in the wrong orientation.
*
* The viewport will need to update the viewport type and/or display set to
* display the resulting data.
*
* The first choice will be a viewport already showing the correct display set,
* but showing it as a stack.
*
* Second choice is to see if there is a viewport already showing the right
* orientation for the image, but the wrong display set. This fixes the
* case where the user is in MPR and a viewport other than active should be
* the one to change to display the iamge.
*
* Final choice is to use the provide activeViewportId. This will cover
* changes to/from video and wsi viewports and other cases where no
* viewport is really even close to being able to display the measurement.
*/
public findUpdateableViewportConfiguration(activeViewportId: string, measurement) {
const { metadata, displaySetInstanceUID } = measurement;
const { volumeId, referencedImageId } = metadata;
const { displaySetService, viewportGridService } = this.servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
let { viewportType } = displaySet;
if (!viewportType) {
if (referencedImageId && !displaySet.isReconstructable) {
viewportType = csEnums.ViewportType.STACK;
} else if (volumeId) {
viewportType = 'volume';
}
}
// Find viewports that could be updated to be volumes to show this view
// That prefers a viewport already showing the right display set.
if (volumeId) {
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
if (viewport?.isReferenceViewable(metadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
}
}
// Find a viewport in the correct orientation showing a different display set
// which could be used to display the annotation.
const altMetadata = { ...metadata, volumeId: null, referencedImageId: null };
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
const viewportDisplaySetUID = viewportGridService.getDisplaySetsUIDsForViewport(id)?.[0];
if (!viewportDisplaySetUID || !viewport) {
continue;
}
if (volumeId) {
altMetadata.volumeId = viewportDisplaySetUID;
}
altMetadata.FrameOfReferenceUID = this._getFrameOfReferenceUID(viewportDisplaySetUID);
if (viewport.isReferenceViewable(altMetadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
}
// Just display in the active viewport
return {
viewportId: activeViewportId,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
/** /**
* Sets the image data for the given viewport. * Sets the image data for the given viewport.
*/ */
@ -626,7 +709,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// is being used to navigate to the initial view position for measurement // is being used to navigate to the initial view position for measurement
// navigation and other navigation forcing specific views. // navigation and other navigation forcing specific views.
let initialImageIndexToUse = let initialImageIndexToUse =
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex; presentations?.positionPresentation?.initialImageIndex ?? <number>initialImageIndex;
const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions(); const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions();
@ -662,7 +745,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
initialImageIndexToUse = imageIds.indexOf(referencedImageId); initialImageIndexToUse = imageIds.indexOf(referencedImageId);
} }
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) { if (
initialImageIndexToUse === undefined ||
initialImageIndexToUse === null ||
initialImageIndexToUse < 0
) {
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
} }
@ -1085,8 +1172,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { dimensions, spacing } = imageVolume; const { dimensions, spacing } = imageVolume;
const slabThickness = Math.sqrt( const slabThickness = Math.sqrt(
Math.pow(dimensions[0] * spacing[0], 2) + Math.pow(dimensions[0] * spacing[0], 2) +
Math.pow(dimensions[1] * spacing[1], 2) + Math.pow(dimensions[1] * spacing[1], 2) +
Math.pow(dimensions[2] * spacing[2], 2) Math.pow(dimensions[2] * spacing[2], 2)
); );
return slabThickness; return slabThickness;
@ -1214,7 +1301,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
): void { ): void {
const viewRef = positionPresentation?.viewReference; const viewRef = positionPresentation?.viewReference;
if (viewRef) { if (viewRef) {
if (viewport.isReferenceViewable(viewRef, WITH_NAVIGATION)) { // The orientation can be updated here to navigate to the specified
// measurement or previous item, but this will not switch to volume
// or to stack from the other type
if (viewport.isReferenceViewable(viewRef, WITH_ORIENTATION)) {
viewport.setViewReference(viewRef); viewport.setViewReference(viewRef);
} else { } else {
console.warn('Unable to apply reference viewable', viewRef); console.warn('Unable to apply reference viewable', viewRef);

View File

@ -3,20 +3,17 @@ import { Enums } from '@cornerstonejs/core';
import OrientationAxis = Enums.OrientationAxis; import OrientationAxis = Enums.OrientationAxis;
export const isReferenceViewable = ({ export const isReferenceViewable = (servicesManager, viewportId, reference, viewportOptions?) => {
viewportId,
reference,
viewportOptions,
servicesManager,
}) => {
const { cornerstoneViewportService, displaySetService } = servicesManager.services; const { cornerstoneViewportService, displaySetService } = servicesManager.services;
if (!viewportOptions) { if (!viewportOptions) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
// we can make a customization for this to allow // we can make a customization for this to allow specific settings
// The annotation can be seen either via navigation or by changing to a volume
const isViewable = viewport.isReferenceViewable(reference, { const isViewable = viewport.isReferenceViewable(reference, {
withNavigation: true, withNavigation: true,
asVolume: true,
}); });
return isViewable; return isViewable;

View File

@ -83,7 +83,7 @@ const Bidirectional = {
function getMappedAnnotations(annotation, displaySetService) { function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats = {} } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);

View File

@ -84,7 +84,7 @@ const CircleROI = {
function getMappedAnnotations(annotation, displaySetService) { function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats={} } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);

View File

@ -92,6 +92,11 @@ function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
if( !cachedStats ) {
return;
}
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {

View File

@ -3,7 +3,6 @@ import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible'; import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { config } from '@cornerstonejs/tools/annotation';
const Length = { const Length = {
toAnnotation: measurement => {}, toAnnotation: measurement => {},
@ -78,7 +77,6 @@ const Length = {
isLocked, isLocked,
isVisible, isVisible,
metadata, metadata,
// color,
referenceSeriesUID: SeriesInstanceUID, referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID, referenceStudyUID: StudyInstanceUID,
referencedImageId, referencedImageId,

View File

@ -95,6 +95,11 @@ function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
if( !cachedStats ) {
return;
}
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {

View File

@ -211,9 +211,11 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
mappedAnnotations.forEach(mappedAnnotation => { mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation; const { unit, max, SeriesNumber } = mappedAnnotation;
const maxStr = getStatisticDisplayString(max, unit, 'max'); if (Number.isFinite(max)) {
const maxStr = getStatisticDisplayString(max, unit, 'max');
displayText.primary.push(maxStr); displayText.primary.push(maxStr);
}
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
}); });

View File

@ -1,5 +1,36 @@
import * as cornerstone from '@cornerstonejs/core'; import * as cornerstone from '@cornerstonejs/core';
function getDisplaySet({ metadata, displaySetService }) {
const { volumeId } = metadata;
if( volumeId ) {
const displaySet = displaySetService.getDisplaySetsBy(displaySet =>
volumeId.includes(displaySet.uid)
)[0];
if( displaySet ) {
return displaySet;
}
console.warn("Unable to find volumeId", volumeId);
metadata.volumeId = null;
}
if (!metadata.FrameOfReferenceUID) {
throw new Error(
'No volumeId and no FrameOfReferenceUID provided. Could not find matching displaySet.'
);
}
const displaySet = Array.from(displaySetService.getDisplaySetCache().values()).find(
ds => ds.instance?.FrameOfReferenceUID === metadata.FrameOfReferenceUID
);
if (!displaySet) {
throw new Error('Could not find matching displaySet for the provided FrameOfReferenceUID.');
}
return displaySet;
}
/** /**
* It checks if the imageId is provided then it uses it to query * It checks if the imageId is provided then it uses it to query
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID. * the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
@ -13,11 +44,7 @@ export default function getSOPInstanceAttributes(imageId, displaySetService, ann
} }
const { metadata } = annotation; const { metadata } = annotation;
const { volumeId } = metadata; const displaySet = getDisplaySet({ metadata, displaySetService });
const displaySet = displaySetService.getDisplaySetsBy(displaySet =>
volumeId.includes(displaySet.uid)
)[0];
const { StudyInstanceUID, SeriesInstanceUID } = displaySet; const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
return { return {

View File

@ -36,7 +36,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/i18n": "3.11.0-beta.114", "@ohif/i18n": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4", "dicomweb-client": "^0.10.4",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -22,7 +22,7 @@ const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0'; const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0';
const ImplementationVersionName = 'OHIF-VIEWER-2.0.0'; const ImplementationVersionName = 'OHIF-3.11.0';
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
const metadataProvider = classes.MetadataProvider; const metadataProvider = classes.MetadataProvider;

View File

@ -383,8 +383,6 @@ function PanelStudyBrowser({
// Set the activeTabName and expand the study // Set the activeTabName and expand the study
const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs); const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs);
if (!thumbnailLocation) { if (!thumbnailLocation) {
console.warn('jumpToThumbnail: displaySet thumbnail not found.');
return; return;
} }
const { tabName, StudyInstanceUID } = thumbnailLocation; const { tabName, StudyInstanceUID } = thumbnailLocation;

View File

@ -5,7 +5,6 @@ const getViewportModule = () => {
{ {
name: 'chartViewport', name: 'chartViewport',
component: LineChartViewport, component: LineChartViewport,
isReferenceViewable: () => false,
}, },
]; ];
}; };

View File

@ -6,12 +6,8 @@ import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber'; import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber';
import { getSeriesDateTime } from './getCurrentDicomDateTime'; import { getSeriesDateTime } from './getCurrentDicomDateTime';
const { const { filterAnd, filterMeasurementsByStudyUID, filterMeasurementsBySeriesUID } =
filterAnd, utils.MeasurementFilters;
filterMeasurementsByStudyUID,
filterMeasurementsBySeriesUID,
filterPlanarMeasurement,
} = utils.MeasurementFilters;
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) { async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
const { measurementService, displaySetService } = servicesManager.services; const { measurementService, displaySetService } = servicesManager.services;
@ -25,8 +21,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
trackedSeries, trackedSeries,
measurementFilter = filterAnd( measurementFilter = filterAnd(
filterMeasurementsByStudyUID(StudyInstanceUID), filterMeasurementsByStudyUID(StudyInstanceUID),
filterMeasurementsBySeriesUID(trackedSeries), filterMeasurementsBySeriesUID(trackedSeries)
filterPlanarMeasurement
), ),
defaultSaveTitle = 'Create Report', defaultSaveTitle = 'Create Report',
} = ctx; } = ctx;

View File

@ -32,7 +32,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -32,7 +32,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -32,14 +32,14 @@
"start": "yarn run dev" "start": "yarn run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.30.3", "@cornerstonejs/tools": "^3.32.5",
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.114", "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.114",
"@ohif/extension-default": "3.11.0-beta.114", "@ohif/extension-default": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -21,7 +21,16 @@ const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext); const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
const SR_SOPCLASSHANDLERID = '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr'; const SR_SOP_CLASS_HANDLER_ID =
'@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr';
const COMPREHENSIVE_3D_SR_SOP_CLASS_HANDLER_ID =
'@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d';
const hasValidSOPClassHandlerId = displaySet => {
return [SR_SOP_CLASS_HANDLER_ID, COMPREHENSIVE_3D_SR_SOP_CLASS_HANDLER_ID].includes(
displaySet.SOPClassHandlerId
);
};
/** /**
* *
@ -320,18 +329,14 @@ function TrackedMeasurementsContextProvider(
// The issue here is that this handler in TrackedMeasurementsContext // The issue here is that this handler in TrackedMeasurementsContext
// ends up occurring before the Viewport is created, so the displaySet // ends up occurring before the Viewport is created, so the displaySet
// is not loaded yet, and isRehydratable is undefined unless we call load(). // is not loaded yet, and isRehydratable is undefined unless we call load().
if ( if (hasValidSOPClassHandlerId(displaySet) && !displaySet.isLoaded && displaySet.load) {
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
!displaySet.isLoaded &&
displaySet.load
) {
await displaySet.load(); await displaySet.load();
} }
// Magic string // Magic string
// load function added by our sopClassHandler module // load function added by our sopClassHandler module
if ( if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && hasValidSOPClassHandlerId(displaySet) &&
displaySet.isRehydratable === true && displaySet.isRehydratable === true &&
!displaySet.isHydrated !displaySet.isHydrated
) { ) {

View File

@ -29,7 +29,7 @@ function getViewportModule({ servicesManager, commandsManager, extensionManager
{ {
name: 'cornerstone-tracked', name: 'cornerstone-tracked',
component: ExtendedOHIFCornerstoneTrackingViewport, component: ExtendedOHIFCornerstoneTrackingViewport,
isReferenceViewable: props => utils.isReferenceViewable({ ...props, servicesManager }), isReferenceViewable: utils.isReferenceViewable.bind(null, servicesManager),
}, },
]; ];
} }

View File

@ -13,8 +13,7 @@ import {
import { useTrackedMeasurements } from '../getContextModule'; import { useTrackedMeasurements } from '../getContextModule';
import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal'; import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal';
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } = const { filterMeasurementsBySeriesUID, filterAny } = utils.MeasurementFilters;
utils.MeasurementFilters;
function PanelMeasurementTableTracking(props) { function PanelMeasurementTableTracking(props) {
const [viewportGrid] = useViewportGrid(); const [viewportGrid] = useViewportGrid();
@ -23,9 +22,7 @@ function PanelMeasurementTableTracking(props) {
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context; const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const measurementFilter = trackedStudy const measurementFilter = trackedStudy ? filterMeasurementsBySeriesUID(trackedSeries) : filterAny;
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
: filterPlanarMeasurement;
const onUntrackConfirm = () => { const onUntrackConfirm = () => {
sendTrackedMeasurementsEvent('UNTRACK_ALL', {}); sendTrackedMeasurementsEvent('UNTRACK_ALL', {});

View File

@ -35,25 +35,10 @@ function TrackedCornerstoneViewport(
const { SeriesInstanceUID } = displaySet; const { SeriesInstanceUID } = displaySet;
const updateIsTracked = useCallback(() => { const updateIsTracked = useCallback(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) {
// A current image id will only exist for volume viewports that can have measurements tracked.
// Typically these are those volume viewports for the series of acquisition.
const currentImageId = viewport?.getCurrentImageId();
if (!currentImageId) {
if (isTracked) {
setIsTracked(false);
}
return;
}
}
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked); setIsTracked(!isTracked);
} }
}, [isTracked, trackedMeasurements, viewportId, SeriesInstanceUID]); }, [isTracked, SeriesInstanceUID, trackedSeries]);
const onElementEnabled = useCallback( const onElementEnabled = useCallback(
evt => { evt => {

View File

@ -32,7 +32,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -32,7 +32,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -42,8 +42,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.16.0", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.16.0", "@cornerstonejs/tools": "^3.32.5",
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone": "3.11.0-beta.114", "@ohif/extension-cornerstone": "3.11.0-beta.114",
"@ohif/extension-default": "3.11.0-beta.114", "@ohif/extension-default": "3.11.0-beta.114",

View File

@ -27,6 +27,7 @@ const tracked = {
const dicomsr = { const dicomsr = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr',
sopClassHandler3D: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d',
viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr',
}; };
@ -251,7 +252,7 @@ function modeFactory() {
}, },
{ {
namespace: dicomsr.viewport, namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler], displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D],
}, },
{ {
namespace: dicomvideo.viewport, namespace: dicomvideo.viewport,
@ -284,6 +285,7 @@ function modeFactory() {
ohif.sopClassHandler, ohif.sopClassHandler,
dicompdf.sopClassHandler, dicompdf.sopClassHandler,
dicomsr.sopClassHandler, dicomsr.sopClassHandler,
dicomsr.sopClassHandler3D,
], ],
hotkeys: { hotkeys: {
name: 'basic-test-hotkeys', name: 'basic-test-hotkeys',

View File

@ -256,13 +256,12 @@ function modeFactory({ modeConfiguration }) {
displaySetsToDisplay: [ displaySetsToDisplay: [
ohif.sopClassHandler, ohif.sopClassHandler,
dicomvideo.sopClassHandler, dicomvideo.sopClassHandler,
dicomsr.sopClassHandler3D,
ohif.wsiSopClassHandler, ohif.wsiSopClassHandler,
], ],
}, },
{ {
namespace: dicomsr.viewport, namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler], displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D],
}, },
{ {
namespace: dicompdf.viewport, namespace: dicompdf.viewport,

View File

@ -34,8 +34,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.16.0", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.16.0", "@cornerstonejs/tools": "^3.32.5",
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.114", "@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.114",
"@ohif/extension-ultrasound-pleura-bline": "3.11.0-beta.114", "@ohif/extension-ultrasound-pleura-bline": "3.11.0-beta.114",

View File

@ -122,6 +122,7 @@
}, },
"resolutions": { "resolutions": {
"commander": "8.3.0", "commander": "8.3.0",
"dcmjs": "0.43.1",
"path-to-regexp": "0.1.12", "path-to-regexp": "0.1.12",
"nth-check": "^2.1.1", "nth-check": "^2.1.1",
"trim-newlines": "^5.0.0", "trim-newlines": "^5.0.0",

View File

@ -53,7 +53,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.30.3", "@cornerstonejs/dicom-image-loader": "^3.32.5",
"@emotion/serialize": "^1.1.3", "@emotion/serialize": "^1.1.3",
"@ohif/core": "3.11.0-beta.114", "@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone": "3.11.0-beta.114", "@ohif/extension-cornerstone": "3.11.0-beta.114",
@ -79,7 +79,7 @@
"classnames": "^2.3.2", "classnames": "^2.3.2",
"core-js": "*", "core-js": "*",
"cornerstone-math": "^0.1.9", "cornerstone-math": "^0.1.9",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"detect-gpu": "^4.0.16", "detect-gpu": "^4.0.16",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "^1.7.0",

View File

@ -26,7 +26,7 @@ window.config = {
// filterQueryParam: false, // filterQueryParam: false,
// Uses the ohif datasource as the default - this requires that KHEOPS be // Uses the ohif datasource as the default - this requires that KHEOPS be
// configured with an OHIF path to .../viewer/dicomwebproxy // configured with an OHIF path to .../viewer/dicomwebproxy
defaultDataSourceName: 'dicomweb', defaultDataSourceName: 'ohif',
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */ /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
// dangerouslyUseDynamicConfig: { // dangerouslyUseDynamicConfig: {
// enabled: true, // enabled: true,
@ -40,7 +40,7 @@ window.config = {
dataSources: [ dataSources: [
{ {
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb', sourceName: 'ohif',
configuration: { configuration: {
friendlyName: 'AWS S3 Static wado server', friendlyName: 'AWS S3 Static wado server',
name: 'aws', name: 'aws',
@ -54,47 +54,15 @@ window.config = {
supportsFuzzyMatching: false, supportsFuzzyMatching: false,
supportsWildcard: true, supportsWildcard: true,
staticWado: true, staticWado: true,
singlepart: 'bulkdata,video', singlepart: 'video,pdf',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
bulkDataURI: { bulkDataURI: {
enabled: true, enabled: true,
relativeResolution: 'studies', relativeResolution: 'studies',
transform: url => url.replace('/pixeldata.mp4', '/rendered'), transform: url => url.replace('/pixeldata.mp4', '/rendered'),
}, },
omitQuotationForMultipartRequest: true,
}, },
}, },
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'ohif2',
configuration: {
friendlyName: 'AWS S3 Static wado secondary server',
name: 'aws',
wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
},
omitQuotationForMultipartRequest: true,
},
},
{ {
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'ohif3', sourceName: 'ohif3',

View File

@ -1,5 +1,5 @@
import React, { useEffect, useCallback, useRef } from 'react'; import React, { useEffect, useCallback, useRef } from 'react';
import { Types, MeasurementService } from '@ohif/core'; import { Types } from '@ohif/core';
import { ViewportGrid, ViewportPane } from '@ohif/ui-next'; import { ViewportGrid, ViewportPane } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next'; import { useViewportGrid } from '@ohif/ui-next';
import EmptyViewport from './EmptyViewport'; import EmptyViewport from './EmptyViewport';
@ -14,13 +14,8 @@ function ViewerViewportGrid(props: withAppTypes) {
const { numCols, numRows } = layout; const { numCols, numRows } = layout;
const layoutHash = useRef(null); const layoutHash = useRef(null);
const { const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
displaySetService, servicesManager.services;
measurementService,
hangingProtocolService,
uiNotificationService,
customizationService,
} = servicesManager.services;
const generateLayoutHash = () => `${numCols}-${numRows}`; const generateLayoutHash = () => `${numCols}-${numRows}`;
@ -32,9 +27,9 @@ function ViewerViewportGrid(props: withAppTypes) {
*/ */
const updateDisplaySetsFromProtocol = ( const updateDisplaySetsFromProtocol = (
protocol: Types.HangingProtocol.Protocol, _protocol: Types.HangingProtocol.Protocol,
stage, stage,
activeStudyUID, _activeStudyUID,
viewportMatchDetails viewportMatchDetails
) => { ) => {
const availableDisplaySets = displaySetService.getActiveDisplaySets(); const availableDisplaySets = displaySetService.getActiveDisplaySets();
@ -154,100 +149,6 @@ function ViewerViewportGrid(props: withAppTypes) {
} }
}, [viewportGridService, generateLayoutHash]); }, [viewportGridService, generateLayoutHash]);
useEffect(() => {
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
event => {
const { viewportId, measurement, isConsumed } = event;
if (isConsumed) {
return;
}
const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement;
const { viewports } = viewportGridService.getState();
// Check if any viewport can display this measurement
let canAnyViewportDisplayMeasurement = false;
viewports.forEach((viewport, id) => {
const displaySetInstanceUIDs = viewport.displaySetInstanceUIDs || [];
const viewportHasDisplaySet = displaySetInstanceUIDs.includes(
referencedDisplaySetInstanceUID
);
// Extract metadata and prepare reference
const { FrameOfReferenceUID, ...metadataRest } = measurement.metadata;
const reference = {
...(viewportHasDisplaySet ? measurement.metadata : metadataRest),
displaySetInstanceUID: referencedDisplaySetInstanceUID,
};
// Check if viewport can display the reference
if (
viewport.isReferenceViewable?.({
viewportId: id,
reference,
})
) {
canAnyViewportDisplayMeasurement = true;
}
});
if (canAnyViewportDisplayMeasurement) {
// Let the viewports handle the jump
return;
}
// Need to change layouts since no viewport consumed the event
const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID);
if (!updatedViewports?.[0]) {
console.warn(
'ViewportGrid::Unable to navigate to viewport containing',
referencedDisplaySetInstanceUID
);
return;
}
// Find the viewport that can display the measurement
const viewport = updatedViewports.find(viewport => {
const gridViewport = viewportGridService.getViewportState(viewport.viewportId);
return gridViewport.isReferenceViewable?.({
viewportId: viewport.viewportId,
reference: {
...measurement.metadata,
displaySetInstanceUID: referencedDisplaySetInstanceUID,
},
viewportOptions: gridViewport.viewportOptions || {},
});
});
if (!viewport) {
console.warn('No suitable viewport found for displaying measurement');
return;
}
// Update stored position presentation
commandsManager.run('updateStoredPositionPresentation', {
viewportId: viewport.viewportId,
displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
referencedImageId: measurement.referencedImageId,
options: {
...measurement.metadata,
},
});
event.consume();
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
}
);
return () => {
unsubscribe();
};
}, [viewports, _getUpdatedViewports]);
const onDropHandler = (viewportId, { displaySetInstanceUID }) => { const onDropHandler = (viewportId, { displaySetInstanceUID }) => {
const { viewportGridService } = servicesManager.services; const { viewportGridService } = servicesManager.services;
const customOnDropHandler = customizationService.getCustomization('customOnDropHandler'); const customOnDropHandler = customizationService.getCustomization('customOnDropHandler');
@ -267,23 +168,6 @@ function ViewerViewportGrid(props: withAppTypes) {
viewportGridService.publishViewportOnDropHandled({ displaySetInstanceUID }); viewportGridService.publishViewportOnDropHandled({ displaySetInstanceUID });
}; };
// Store previous isReferenceViewable values to avoid infinite loops
const prevReferenceViewableMap = useRef(new Map());
// Track viewports that need isReferenceViewable updates
const viewportsToUpdate = useRef(new Map());
// Apply isReferenceViewable updates in an effect, not during render
useEffect(() => {
const updates = viewportsToUpdate.current;
if (updates.size > 0) {
updates.forEach((isReferenceViewable, viewportId) => {
viewportGridService.setIsReferenceViewable(viewportId, isReferenceViewable);
prevReferenceViewableMap.current.set(viewportId, isReferenceViewable);
});
viewportsToUpdate.current.clear();
}
});
const getViewportPanes = useCallback(() => { const getViewportPanes = useCallback(() => {
const viewportPanes = []; const viewportPanes = [];
@ -315,27 +199,12 @@ function ViewerViewportGrid(props: withAppTypes) {
return !displaySet?.unsupported; return !displaySet?.unsupported;
}); });
const { component: ViewportComponent, isReferenceViewable } = _getViewportComponent( const { component: ViewportComponent } = _getViewportComponent(
displaySets, displaySets,
viewportComponents, viewportComponents,
uiNotificationService uiNotificationService
); );
// Only queue isReferenceViewable updates if it's changed to avoid render loops
// We need to handle both function and non-function values
if (viewportId) {
const prevValue = prevReferenceViewableMap.current.get(viewportId);
const isFunction = typeof isReferenceViewable === 'function';
const isSameFunction = isFunction && typeof prevValue === 'function';
// For non-functions, compare directly. For functions, we treat them as always different
// (this is conservative but safe)
if (!isSameFunction && prevValue !== isReferenceViewable) {
// Queue the update instead of doing it during render
viewportsToUpdate.current.set(viewportId, isReferenceViewable);
}
}
// look inside displaySets to see if they need reRendering // look inside displaySets to see if they need reRendering
const displaySetsNeedsRerendering = displaySets.some(displaySet => { const displaySetsNeedsRerendering = displaySets.some(displaySet => {
return displaySet.needsRerendering; return displaySet.needsRerendering;
@ -462,8 +331,8 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
throw new Error('displaySetsToDisplay is null'); throw new Error('displaySetsToDisplay is null');
} }
if (viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)) { if (viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)) {
const { component, isReferenceViewable } = viewportComponents[i]; const { component } = viewportComponents[i];
return { component, isReferenceViewable }; return { component };
} }
} }
@ -474,7 +343,7 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
type: 'error', type: 'error',
}); });
return { component: EmptyViewport, isReferenceViewable: () => false }; return { component: EmptyViewport };
} }
export default ViewerViewportGrid; export default ViewerViewportGrid;

View File

@ -37,15 +37,15 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/core": "^3.30.3", "@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/dicom-image-loader": "^3.30.3", "@cornerstonejs/dicom-image-loader": "^3.32.5",
"@ohif/ui": "3.11.0-beta.114", "@ohif/ui": "3.11.0-beta.114",
"cornerstone-math": "0.1.9", "cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21" "dicom-parser": "^1.8.21"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"dcmjs": "^0.42.0", "dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4", "dicomweb-client": "^0.10.4",
"gl-matrix": "^3.4.3", "gl-matrix": "^3.4.3",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",

View File

@ -74,10 +74,11 @@ const EVENTS = {
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added', RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed', MEASUREMENT_REMOVED: 'event::measurement_removed',
MEASUREMENTS_CLEARED: 'event::measurements_cleared', MEASUREMENTS_CLEARED: 'event::measurements_cleared',
// Give the viewport a chance to jump to the measurement /**
JUMP_TO_MEASUREMENT_VIEWPORT: 'event:jump_to_measurement_viewport', * Indicate some viewport should be jumped to. This will have to be implemented
// Give the layout a chance to jump to the measurement * by a single handler that can look at all viewports to decide who should handle it.
JUMP_TO_MEASUREMENT_LAYOUT: 'event:jump_to_measurement_layout', */
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
}; };
const VALUE_TYPES = { const VALUE_TYPES = {
@ -431,11 +432,7 @@ class MeasurementService extends PubSubService {
return; return;
} }
let internalUID = data.id; const internalUID = data.uid || guid();
if (!internalUID) {
internalUID = guid();
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
}
const annotationData = data.annotation.data; const annotationData = data.annotation.data;
@ -497,7 +494,7 @@ class MeasurementService extends PubSubService {
mapping => mapping.annotationType === annotationType mapping => mapping.annotationType === annotationType
); );
if (!sourceMapping) { if (!sourceMapping) {
console.log('No source mapping', source); console.log('No source mapping', source.uid, annotationType, source);
return; return;
} }
const { toMeasurementSchema } = sourceMapping; const { toMeasurementSchema } = sourceMapping;
@ -656,19 +653,7 @@ class MeasurementService extends PubSubService {
} }
/** /**
* This method calls the subscriptions for JUMP_TO_MEASUREMENT_VIEWPORT * This method calls the subscription for JUMP_TO_MEASUREMENT
* and JUMP_TO_MEASUREMENT_LAYOUT. There are two events which are
* fired because there are two different items which might want to handle
* the event. First, there might already be a viewport which can handle
* the event. If so, then the layout doesn't need to necessarily change.
* This is communicated by the isConsumed value on the event itself.
* Otherwise, the layout itself may need to be navigated to in order
* to provide a viewport which can show the given measurement.
*
* When a viewport decides to apply the event, it should call the consume()
* method on the event, so that other listeners know they do not need to
* navigate. This does NOT affect whether the layout event is fired, and
* merely causes it to fire the event with the isConsumed set to true.
*/ */
public jumpToMeasurement(viewportId: string, measurementUID: string): void { public jumpToMeasurement(viewportId: string, measurementUID: string): void {
@ -678,16 +663,14 @@ class MeasurementService extends PubSubService {
log.warn(`No measurement uid, or unable to find by uid.`); log.warn(`No measurement uid, or unable to find by uid.`);
return; return;
} }
const consumableEvent = this.createConsumableEvent({ const event = {
viewportId, viewportId,
measurement, measurement,
}); };
// Important: we should broadcast the layout event first, since // A single handler will decide on which window to jump to, previously
// in the layout there might be a viewport that we can match and choose // this was handled by a consumable event
// and jump in it before we decide on changing the orientation of different viewports this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT, event);
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, consumableEvent);
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, consumableEvent);
} }
_getSourceUID(name, version) { _getSourceUID(name, version) {

View File

@ -45,10 +45,6 @@ class ViewportGridService extends PubSubService {
return this.presentationIdProviders.get(id); return this.presentationIdProviders.get(id);
} }
public setIsReferenceViewable(viewportId: string, isReferenceViewable: boolean): void {
this.serviceImplementation._setIsReferenceViewable(viewportId, isReferenceViewable);
}
public getPresentationId(id: string, viewportId: string): string | null { public getPresentationId(id: string, viewportId: string): string | null {
const state = this.getState(); const state = this.getState();
const viewport = state.viewports.get(viewportId); const viewport = state.viewports.get(viewportId);
@ -104,7 +100,6 @@ class ViewportGridService extends PubSubService {
set: setImplementation, set: setImplementation,
getNumViewportPanes: getNumViewportPanesImplementation, getNumViewportPanes: getNumViewportPanesImplementation,
setViewportIsReady: setViewportIsReadyImplementation, setViewportIsReady: setViewportIsReadyImplementation,
setIsReferenceViewable: setIsReferenceViewableImplementation,
getViewportState: getViewportStateImplementation, getViewportState: getViewportStateImplementation,
}): void { }): void {
if (getViewportStateImplementation) { if (getViewportStateImplementation) {
@ -139,9 +134,6 @@ class ViewportGridService extends PubSubService {
if (setViewportIsReadyImplementation) { if (setViewportIsReadyImplementation) {
this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation; this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation;
} }
if (setIsReferenceViewableImplementation) {
this.serviceImplementation._setIsReferenceViewable = setIsReferenceViewableImplementation;
}
} }
public publishViewportsReady() { public publishViewportsReady() {

View File

@ -29,6 +29,9 @@ export type DisplaySet = {
/** A fetch method to get the thumbnail */ /** A fetch method to get the thumbnail */
getThumbnailSrc?(imageId?: string): Promise<string>; getThumbnailSrc?(imageId?: string): Promise<string>;
/** An opaque type of this viewport, used internally to specify which viewport to use */
viewportType;
/** /**
* A fetch URL to display the content. This is used for content such as * A fetch URL to display the content. This is used for content such as
* pdf display. * pdf display.

View File

@ -45,10 +45,6 @@ export interface ViewportGridState {
activeViewportId: string | null; activeViewportId: string | null;
layout: Layout; layout: Layout;
isHangingProtocolLayout: boolean; isHangingProtocolLayout: boolean;
isReferenceViewable: (props: {
viewportId: string;
reference: Record<string, unknown>;
}) => boolean;
viewports: GridViewports; viewports: GridViewports;
} }

View File

@ -9,6 +9,10 @@ summary: Migration guide for OHIF 3.11 additional changes
* **`connectToolsToMeasurementService` parameters:** The `connectToolsToMeasurementService` function from the `@ohif/cornerstone-extensions` now take different arguments. * **`connectToolsToMeasurementService` parameters:** The `connectToolsToMeasurementService` function from the `@ohif/cornerstone-extensions` now take different arguments.
* **`data-viewportId`** The `data-viewportId` naming was not compliant with react and was causing warnings. Rename references to `data-viewportid`. * **`data-viewportId`** The `data-viewportId` naming was not compliant with react and was causing warnings. Rename references to `data-viewportid`.
* **`setIsReferenceViewable`** is no longer available or required by ViewportGridService. Instead, the cornerstone viewports
themselves provide the isReferenceViewable. This occurs because there were a lot more deciding issues to navigate to viewports than could be added to viewport grid service.
* **`JUMP_TO_MEASUREMENT_VIEWPORT` and `JUMP_TO_MEASUREMENT_LAYOUT`** are combined into `JUMP_TO_MEASUREMENT` with no
consume event. Only the single event is fired. This will need to be handled to redirect the changes to the appropriate viewport type as it was not possible to figure that out with generic information available in `ViewportGridService`
**Migration Steps:** **Migration Steps:**

View File

@ -31,8 +31,7 @@ There are seven events that get publish in `MeasurementService`:
| RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) | | RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) |
| MEASUREMENT_REMOVED | Fires when a measurement is removed | | MEASUREMENT_REMOVED | Fires when a measurement is removed |
| MEASUREMENTS_CLEARED | Fires when all measurements are deleted | | MEASUREMENTS_CLEARED | Fires when all measurements are deleted |
| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. | | JUMP_TO_MEASUREMENT | Fires when a measurement is requested to be jumped to |
| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. |
## API ## API

View File

@ -246,11 +246,9 @@ const DefaultFallback = ({
const ErrorBoundary = ({ const ErrorBoundary = ({
context = 'OHIF', context = 'OHIF',
onReset = () => {}, onReset = () => {},
onError = () => {}, onError = _error => {},
fallbackComponent: FallbackComponent = DefaultFallback, fallbackComponent: FallbackComponent = DefaultFallback,
children, children,
fallbackRoute = null,
isPage,
}: ErrorBoundaryProps) => { }: ErrorBoundaryProps) => {
const [error, setError] = useState<ErrorBoundaryError | null>(null); const [error, setError] = useState<ErrorBoundaryError | null>(null);
@ -276,8 +274,8 @@ const ErrorBoundary = ({
event.preventDefault(); event.preventDefault();
clearTimeout(errorTimeout); clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => { errorTimeout = setTimeout(() => {
setError(event.reason); setError(event.reason || event);
onErrorHandler(event.reason, null); onErrorHandler(event.reason || event, null);
}, 100); }, 100);
}; };
@ -291,7 +289,10 @@ const ErrorBoundary = ({
}; };
}, []); }, []);
const onErrorHandler = (error: ErrorBoundaryError, componentStack: string | null) => { const onErrorHandler = (
error: ErrorBoundaryError | ErrorEvent,
componentStack: string | null
) => {
console.debug(`${context} Error Boundary`, error, componentStack, context); console.debug(`${context} Error Boundary`, error, componentStack, context);
onError(error, componentStack || '', context); onError(error, componentStack || '', context);
}; };

View File

@ -125,6 +125,7 @@ interface ViewportGridApi {
setViewportGridSizeChanged: (props: any) => void; setViewportGridSizeChanged: (props: any) => void;
publishViewportsReady: () => void; publishViewportsReady: () => void;
getDisplaySetsUIDsForViewport: (viewportId: string) => string[]; getDisplaySetsUIDsForViewport: (viewportId: string) => string[];
isReferenceViewable: (viewportId: string, viewRef, options?) => boolean;
} }
// Update the context type // Update the context type
@ -142,19 +143,6 @@ interface ViewportGridProviderProps {
export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) { export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) {
const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => { const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => {
switch (action.type) { switch (action.type) {
case 'SET_IS_REFERENCE_VIEWABLE': {
const { viewportId, isReferenceViewable } = action.payload;
const viewports = new Map(state.viewports);
const viewport = viewports.get(viewportId);
if (!viewport) {
return;
}
viewports.set(viewportId, {
...viewport,
isReferenceViewable,
});
return { ...state, viewports };
}
case 'SET_ACTIVE_VIEWPORT_ID': { case 'SET_ACTIVE_VIEWPORT_ID': {
return { ...state, ...{ activeViewportId: action.payload } }; return { ...state, ...{ activeViewportId: action.payload } };
} }
@ -392,13 +380,6 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
[dispatch] [dispatch]
); );
const setIsReferenceViewable = useCallback(
(viewportId, isReferenceViewable) => {
dispatch({ type: 'SET_IS_REFERENCE_VIEWABLE', payload: { viewportId, isReferenceViewable } });
},
[dispatch]
);
const setDisplaySetsForViewports = useCallback( const setDisplaySetsForViewports = useCallback(
viewports => viewports =>
dispatch({ dispatch({
@ -495,7 +476,7 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
getState, getState,
setActiveViewportId, setActiveViewportId,
setDisplaySetsForViewports, setDisplaySetsForViewports,
setIsReferenceViewable, isReferenceViewable: () => false,
setLayout, setLayout,
reset, reset,
onModeExit: reset, onModeExit: reset,
@ -511,7 +492,6 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
service, service,
setActiveViewportId, setActiveViewportId,
setDisplaySetsForViewports, setDisplaySetsForViewports,
setIsReferenceViewable,
setLayout, setLayout,
reset, reset,
set, set,
@ -527,8 +507,8 @@ export function ViewportGridProvider({ children, service }: ViewportGridProvider
setActiveViewportId: index => service.setActiveViewportId(index), setActiveViewportId: index => service.setActiveViewportId(index),
setDisplaySetsForViewport: props => service.setDisplaySetsForViewports([props]), setDisplaySetsForViewport: props => service.setDisplaySetsForViewports([props]),
setDisplaySetsForViewports: props => service.setDisplaySetsForViewports(props), setDisplaySetsForViewports: props => service.setDisplaySetsForViewports(props),
setIsReferenceViewable: (viewportId, isReferenceViewable) => isReferenceViewable: (viewportId, isReferenceViewable, options) =>
service.setIsReferenceViewable(viewportId, isReferenceViewable), service.isReferenceViewable(viewportId, isReferenceViewable, options),
setLayout: layout => service.setLayout(layout), setLayout: layout => service.setLayout(layout),
getViewportState: viewportId => service.getViewportState(viewportId), getViewportState: viewportId => service.getViewportState(viewportId),
reset: () => service.reset(), reset: () => service.reset(),

View File

@ -25,6 +25,7 @@ const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO;
// Add port constant // Add port constant
const OHIF_PORT = Number(process.env.OHIF_PORT || 3000); const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
const OHIF_OPEN = process.env.OHIF_OPEN !== 'false';
export default defineConfig({ export default defineConfig({
source: { source: {
@ -142,7 +143,7 @@ export default defineConfig({
}, },
server: { server: {
port: OHIF_PORT, port: OHIF_PORT,
open: true, open: OHIF_OPEN,
// Configure proxy // Configure proxy
proxy: { proxy: {
'/dicomweb': { '/dicomweb': {

View File

@ -19,6 +19,9 @@ test('should display multiple segmentation overlays (both SEG and RT)', async ({
await page.getByText('SELECT A SEGMENTATION').click(); await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('2d-tta_nnU-Net_Segmentation').click(); await page.getByTestId('2d-tta_nnU-Net_Segmentation').click();
// A short wait after each overlay is selected to ensure it loads.
await page.waitForTimeout(5000);
// Adding an overlay should not show the LOAD button. // Adding an overlay should not show the LOAD button.
assertNumberOfModalityLoadBadges({ page, expectedCount: 0 }); assertNumberOfModalityLoadBadges({ page, expectedCount: 0 });
@ -26,6 +29,9 @@ test('should display multiple segmentation overlays (both SEG and RT)', async ({
await page.getByText('SELECT A SEGMENTATION').click(); await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('Segmentation').click(); await page.getByTestId('Segmentation').click();
// A short wait after each overlay is selected to ensure it loads.
await page.waitForTimeout(5000);
// Adding an overlay should not show the LOAD button. // Adding an overlay should not show the LOAD button.
assertNumberOfModalityLoadBadges({ page, expectedCount: 0 }); assertNumberOfModalityLoadBadges({ page, expectedCount: 0 });
@ -33,6 +39,9 @@ test('should display multiple segmentation overlays (both SEG and RT)', async ({
await page.getByText('SELECT A SEGMENTATION').click(); await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('3d_lowres-tta_nnU-Net_Segmentation').click(); await page.getByTestId('3d_lowres-tta_nnU-Net_Segmentation').click();
// A short wait after each overlay is selected to ensure it loads.
await page.waitForTimeout(5000);
// Adding an overlay should not show the LOAD button. // Adding an overlay should not show the LOAD button.
assertNumberOfModalityLoadBadges({ page, expectedCount: 0 }); assertNumberOfModalityLoadBadges({ page, expectedCount: 0 });
@ -69,11 +78,12 @@ test('should display multiple segmentation overlays (both SEG and RT)', async ({
await page.getByText('SELECT A SEGMENTATION').click(); await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('Series 3 - RTSTRUCT').click(); await page.getByTestId('Series 3 - RTSTRUCT').click();
// A short wait after each overlay is selected to ensure it loads.
await page.waitForTimeout(5000);
// Adding an overlay should not show the LOAD button. // Adding an overlay should not show the LOAD button.
assertNumberOfModalityLoadBadges({ page, expectedCount: 0 }); assertNumberOfModalityLoadBadges({ page, expectedCount: 0 });
await page.waitForTimeout(5000);
await checkForScreenshot({ await checkForScreenshot({
page, page,
screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaySEGsAndRTDisplayed, screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaySEGsAndRTDisplayed,

View File

@ -11,10 +11,14 @@ test('should launch MPR with unhydrated SEG', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click(); await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segNoHydrationThenMPR.segNoHydrationPreMPR); await checkForScreenshot(page, page, screenShotPaths.segNoHydrationThenMPR.segNoHydrationPreMPR);
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segNoHydrationThenMPR.segNoHydrationPostMPR); await checkForScreenshot(page, page, screenShotPaths.segNoHydrationThenMPR.segNoHydrationPostMPR);
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 176 KiB

136
yarn.lock
View File

@ -1319,25 +1319,25 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^3.30.3": "@cornerstonejs/adapters@^3.32.5":
version "3.30.3" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.30.3.tgz#f1b0bac426e2a0916f2f971677291e4c4b3ac4bc" resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.32.5.tgz#bd292827868267d93de1f94b20545f079f09ed1e"
integrity sha512-rlppvJBjty9fkoFpWdRoPYKB2CGtCp+AiTQBk1OnM181Ok7r+NFEoe7e1PARHjbMjrH/Y5vOLnqEon/ABdbm8A== integrity sha512-tpd+sHjE54VOqme83HZ9XqGKztjikrwbrWR4M3e8X+fuR6LJ+qNQ6+YkPdfThzu0KxN+q8mO5aSvGyJo82rRnA==
dependencies: dependencies:
"@babel/runtime-corejs2" "^7.17.8" "@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3" buffer "^6.0.3"
dcmjs "^0.42.0" dcmjs "^0.43.1"
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
ndarray "^1.0.19" ndarray "^1.0.19"
"@cornerstonejs/ai@^3.30.3": "@cornerstonejs/ai@^3.32.5":
version "3.30.3" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.30.3.tgz#88c3a678df1ba06f80010ebfaa98851c80ec43ea" resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.32.5.tgz#6ca644702f034a692f8a309c10c2118e25f0382a"
integrity sha512-KNz/FLhoBZFJkrlMIfmS7PCSwBH9YEcg5Sh0HKw3GxpV6XE1H9tKl2c5oH/DHuJFj8gMC2FRnH6rOx3x+BpOrQ== integrity sha512-j7paST1ounIAvJYS9/qqOsjknjdFcfcNdwCKiwB5KpobL2/BmYNl4kEZNdYXZPhFfQPQvgpW71OedxkDdwaJ2A==
dependencies: dependencies:
"@babel/runtime-corejs2" "^7.17.8" "@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3" buffer "^6.0.3"
dcmjs "^0.42.0" dcmjs "^0.43.1"
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
lodash.clonedeep "^4.5.0" lodash.clonedeep "^4.5.0"
ndarray "^1.0.19" ndarray "^1.0.19"
@ -1369,30 +1369,20 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.7.tgz#784394d05d8cf735640e9c3baa206535ee8e0376" resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.7.tgz#784394d05d8cf735640e9c3baa206535ee8e0376"
integrity sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w== integrity sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w==
"@cornerstonejs/core@^3.16.0": "@cornerstonejs/core@^3.32.5":
version "3.24.0" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.24.0.tgz#6f184b5eedbf5cc2266bb899cf9454cd866b4e4e" resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.32.5.tgz#527cffa6e48810fd2d141fefa714ff6886368b4f"
integrity sha512-Ep5hVrM5XMVuExL2i8tiVuFGqoi1QyaZpU3WzxqhH75nNpckSi/ZYqic4rI3qb/V4IKYv2R+8Gp4Jptl110u4A== integrity sha512-6m/ODfSyM+8/ZigD2wM2wl8yjxmDx9DTen/R5nxCHUe4sqK97PKjfmcN5OK9cOCzwlzqQBZTFow1Ra6UAfkKCA==
dependencies: dependencies:
"@kitware/vtk.js" "32.12.1" "@kitware/vtk.js" "32.12.1"
comlink "^4.4.1" comlink "^4.4.1"
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
loglevel "^1.9.2" loglevel "^1.9.2"
"@cornerstonejs/core@^3.30.3": "@cornerstonejs/dicom-image-loader@^3.32.5":
version "3.30.3" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.30.3.tgz#2f7805c11a4344f21dc1f7a1918a3c7430180802" resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.32.5.tgz#b91bf19b31a307b03766b154f39eabd58caafd36"
integrity sha512-nGfMNpL4VnPnYTDKBWgDxLpWLl/E3QUoEtScZDN5rKSQrocO/WgX2pt2vxBjbfI5NVPtngR3ly/qffg13EeOaw== integrity sha512-fOUkphjdzTG8Flw0XV4bGUEZtbASeE2/Y+HssKr4PiwiDvGcVvVQF7RxyKkvabP3OsZIAaBOhPB8ghKEEXDp2w==
dependencies:
"@kitware/vtk.js" "32.12.1"
comlink "^4.4.1"
gl-matrix "^3.4.3"
loglevel "^1.9.2"
"@cornerstonejs/dicom-image-loader@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.30.3.tgz#2bc741b1421e70efff9019f9b8be1dbdfee50c6e"
integrity sha512-RK6X/MXGMMqj5khvW21/pGcrcYWfcIe0pGmYwE0iAY1zacqwVNB3EwoNfH2EqxyD6Noe0fDondZoW2laSbh+Kw==
dependencies: dependencies:
"@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
@ -1404,34 +1394,25 @@
pako "^2.0.4" pako "^2.0.4"
uuid "^9.0.0" uuid "^9.0.0"
"@cornerstonejs/labelmap-interpolation@^3.30.3": "@cornerstonejs/labelmap-interpolation@^3.32.5":
version "3.30.3" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.30.3.tgz#475f116dce301df4cefdca6728414438b9f1fa12" resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.32.5.tgz#d2808aba8a528bd63522c4759f445ea2603f3686"
integrity sha512-WVPqidTTO9mWaSir0uERHqRmXAOl9CzP5EhAumoR/sZaQv0ZUDhkyOjc8jv8MZftSTRLSnlr0Ovy/Tnfxwl45w== integrity sha512-+E476+jgAAOGUVUcdMD0D2K5/yHfc3rCVCgApfZ9H5mOaEavVHPjGxomFkE5twnBfQIBKgTos3968anl21ziAw==
dependencies: dependencies:
"@itk-wasm/morphological-contour-interpolation" "1.1.0" "@itk-wasm/morphological-contour-interpolation" "1.1.0"
itk-wasm "1.0.0-b.165" itk-wasm "1.0.0-b.165"
"@cornerstonejs/polymorphic-segmentation@^3.30.3": "@cornerstonejs/polymorphic-segmentation@^3.32.5":
version "3.30.3" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.30.3.tgz#b8fdab95df95af7d83b125ce74942fed4c239eaf" resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.32.5.tgz#edfbd9bd7c3060628f0b8f38a9abb1380087a449"
integrity sha512-5LWPRroG6fYdk/CmTKUYyop1M6BUAT5qrXWvMD3xsWMKMW5ScQohRoPZX4hLIfxuhb/ckuiiQwNqixgcDOH2Gw== integrity sha512-MyG1ULFhOEdc8ud+5NJhVUmlBis61K3IWod1UYxZM4VdY7kivbdPbWXaynhnYTUg5OeXGPsETLWdLw/5E32mag==
dependencies: dependencies:
"@icr/polyseg-wasm" "0.4.0" "@icr/polyseg-wasm" "0.4.0"
"@cornerstonejs/tools@^3.16.0": "@cornerstonejs/tools@^3.32.5":
version "3.24.0" version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.24.0.tgz#7e4bb8a8e8a982316e8f53282441f888f26bb70b" resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.32.5.tgz#1b6fa662a862332fc220c4899fab4303abfd4088"
integrity sha512-KYkElmvmFwuskKnp9glUIwiHYyVfr1LCqOjhj1y1+rgzxAqlrfaGmS7y8KHOqGHzQwXMMoaEKzRGRviAS915tg== integrity sha512-h1UWy/u8Cz1QPsMvbNF+rRGwaEdRnGwCbOSZQwLzYOuvaV+rvGAI6l+Xbhe9zUgzE1lSbBigiQjdGHiNtnVWPQ==
dependencies:
"@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1"
lodash.get "^4.4.2"
"@cornerstonejs/tools@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.30.3.tgz#641ae2216c3f17918798bca3f040650bba555ca4"
integrity sha512-E+Zy+1FvRLOeElCwYmuRdrtwBqbctAhrqCVZNgM3n4vVdakakGnuHFxOJ2eO17LdSEQaCX1hGEFrFQ8UbuCsiw==
dependencies: dependencies:
"@types/offscreencanvas" "2019.7.3" "@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1" comlink "^4.4.1"
@ -8745,23 +8726,10 @@ dayjs@^1.10.4:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
dcmjs@^0.29.8: dcmjs@0.43.1, dcmjs@^0.29.8, dcmjs@^0.43.1:
version "0.29.13" version "0.43.1"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.13.tgz#a74df541bf3948366b25630dbe1e8376f108ea3b" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.43.1.tgz#03223e56d52e9ec2c3b8c10afe9200d754befe62"
integrity sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA== integrity sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q==
dependencies:
"@babel/runtime-corejs3" "^7.22.5"
adm-zip "^0.5.10"
gl-matrix "^3.1.0"
lodash.clonedeep "^4.5.0"
loglevelnext "^3.0.1"
ndarray "^1.0.19"
pako "^2.0.4"
dcmjs@^0.42.0:
version "0.42.0"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.42.0.tgz#40a41330ccb00bd2a37f4ed4552874c45975489a"
integrity sha512-N/SXIzMvAjDRgb8vC8G7DP/2BI1QKpa3TwY47Ok34g5y6ZKIcpAR3sf+Y+OBWkQxvnCTncB6B4tkq/ih8xjmXA==
dependencies: dependencies:
"@babel/runtime-corejs3" "^7.22.5" "@babel/runtime-corejs3" "^7.22.5"
adm-zip "^0.5.10" adm-zip "^0.5.10"
@ -14272,11 +14240,6 @@ loglevel@^1.8.1, loglevel@^1.9.2:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08"
integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==
loglevelnext@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==
long@^5.0.0, long@^5.2.3: long@^5.0.0, long@^5.2.3:
version "5.3.2" version "5.3.2"
resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
@ -19673,7 +19636,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -19691,6 +19654,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.1.1: string-width@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@ -19799,7 +19771,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -19820,6 +19792,13 @@ strip-ansi@^4.0.0:
dependencies: dependencies:
ansi-regex "^3.0.0" ansi-regex "^3.0.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1: strip-ansi@^7.0.1:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -21785,7 +21764,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8:
loader-utils "^2.0.0" loader-utils "^2.0.0"
schema-utils "^3.0.0" schema-utils "^3.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -21811,6 +21790,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"