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/
**/.claude/settings.local.json
# Backup files
*~

View File

@ -38,7 +38,7 @@
"resolutions": {
"**/@babel/runtime": "^7.20.13",
"commander": "8.3.0",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicomweb-client": ">=0.10.4",
"nth-check": "^2.1.1",
"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": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}

View File

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

View File

@ -38,7 +38,7 @@
"@ohif/extension-cornerstone": "3.11.0-beta.114",
"@ohif/extension-measurement-tracking": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
}
}

View File

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

View File

@ -63,18 +63,12 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
const updateViewport = useCallback(
newMeasurementSelected => {
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
const { StudyInstanceUID, displaySetInstanceUID } = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
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(
srDisplaySet,
newMeasurementSelected,
@ -258,6 +252,9 @@ async function _getViewportReferencedDisplaySetData(
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!referencedDisplaySet?.images) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {

View File

@ -15,8 +15,12 @@ import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { MetadataProvider: metadataProvider } = classes;
const {
TEXT_ANNOTATION_POSITION,
COMMENT_CODE,
CodeScheme: Cornerstone3DCodeScheme,
} = adaptersSR.Cornerstone3D;
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.
*
@ -236,18 +246,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
measurement => measurement.loaded === false
);
if (
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
if (!unloadedMeasurements.length || newDisplaySet.unsupported) {
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
const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
@ -266,6 +268,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const is3DMeasurement = measurement.coords?.[0]?.ValueType === 'SCOORD3D';
const onBeforeSRAddMeasurement = customizationService.getCustomization(
'onBeforeSRAddMeasurement'
@ -280,9 +283,15 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
// 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);
measurement.loaded = true;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
unloadedMeasurements.splice(j, 1);
continue;
}
@ -469,11 +478,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
* @returns {any} - The processed measurement result.
*/
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
)
) {
if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) {
return _processTID1410Measurement(mergedContentSequence);
}
@ -511,12 +516,25 @@ function _processTID1410Measurement(mergedContentSequence) {
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 = {
loaded: false,
labels: [],
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
coords: [pointDataItem],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
graphicCode,
is3DMeasurement,
pointsLength,
graphicType: pointDataItem.GraphicType,
};
NUMContentItems.forEach(item => {
@ -567,6 +585,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
);
const commentSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === COMMENT_CODE.schemeDesignator &&
item.ConceptNameCodeSequence.CodeValue === COMMENT_CODE.value
);
const measurement = {
loaded: false,
labels: [],
@ -575,6 +599,14 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
};
if (commentSites) {
for (const group of commentSites) {
if (group.TextValue) {
measurement.labels.push({ label: group.TextValue, value: '' });
}
}
}
if (
finding &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
@ -726,4 +758,17 @@ function _getSequenceAsArray(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;

View File

@ -12,7 +12,6 @@ import {
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
@ -57,7 +56,7 @@ export default function init({
'SRSCOORD3DPoint',
POINT,
SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement
SRSCOOR3DProbeMapper.toMeasurement.bind(null, { servicesManager })
);
// 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 {
annotation,
drawing,

View File

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

View File

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

View File

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

View File

@ -11,7 +11,11 @@ const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
* @returns {string} The extracted label.
*/
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(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT

View File

@ -36,6 +36,14 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
ValueType,
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
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
@ -90,6 +98,13 @@ function getRenderableData({ GraphicType, GraphicData, 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;
}
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
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 { vec3 } from 'gl-matrix';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
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';
const { locking } = CsAnnotation;
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 supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
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(
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
@ -88,7 +91,7 @@ export default function hydrateStructuredReport(
// Mapping of legacy datasets is now directly handled by adapters module
const datasetToUse = instance;
// Use dcmjs to generate toolState.
// Use CS3D adapters to generate toolState.
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// 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
// the measurementGroups in the instance.
sopInstanceUIDToImageId,
utilities.imageToWorldCoords,
metaData
);
@ -147,6 +149,9 @@ export default function hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
if (!imageId) {
continue;
}
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
@ -160,10 +165,11 @@ export default function hydrateStructuredReport(
}
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
/**
* 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
@ -173,7 +179,9 @@ export default function hydrateStructuredReport(
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
toolData.uid = guid();
if (!imageId) {
return getReferenceData3D(toolData, servicesManager);
}
const instance = metaData.get('instance', imageId);
const {
@ -183,15 +191,29 @@ export default function hydrateStructuredReport(
// StudyInstanceUID,
} = instance;
return {
referencedImageId: imageId,
FrameOfReferenceUID,
};
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
toolData.uid = guid();
const referenceData = getReferenceData(toolData);
const { imageId } = referenceData;
const annotation = {
annotationUID: toolData.annotation.annotationUID,
data: toolData.annotation.data,
metadata: {
...referenceData,
toolName: annotationType,
referencedImageId: imageId,
FrameOfReferenceUID,
},
};
utilities.updatePlaneRestriction(annotation.data.handles.points, annotation.metadata);
const source = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
@ -225,7 +247,7 @@ export default function hydrateStructuredReport(
locking.setAnnotationLocked(newAnnotationUID, true);
}
if (!imageIds.includes(imageId)) {
if (imageId && !imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
@ -238,3 +260,92 @@ export default function hydrateStructuredReport(
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;
for (let i = 0; i < measurements.length; i++) {
const { TrackingIdentifier } = measurements[i] || {};
if (!TrackingIdentifier) {
console.warn('No tracking identifier for measurement ', measurements[i]);
const measurement = measurements[i];
if (!measurement) {
continue;
}
const { TrackingIdentifier = '', graphicType, graphicCode, pointsLength } = measurement;
if (!TrackingIdentifier && !graphicType) {
console.warn('No tracking identifier or graphicType for measurement ', measurement);
continue;
}
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) {
return true;

View File

@ -34,7 +34,7 @@
"@ohif/extension-default": "3.11.0-beta.114",
"@ohif/i18n": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
}
}

View File

@ -38,10 +38,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.30.3",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@ohif/core": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/ai": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/labelmap-interpolation": "^3.30.3",
"@cornerstonejs/polymorphic-segmentation": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/ai": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/labelmap-interpolation": "^3.32.5",
"@cornerstonejs/polymorphic-segmentation": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",

View File

@ -13,7 +13,6 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import ViewportColorbarsContainer from '../components/ViewportColorbar';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
@ -305,31 +304,6 @@ const OHIFCornerstoneViewport = React.memo(
loadViewportData();
}, [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');
return (
@ -387,54 +361,6 @@ const OHIFCornerstoneViewport = React.memo(
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) {
const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId];
@ -558,16 +484,4 @@ function areEqual(prevProps, nextProps) {
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;

View File

@ -146,6 +146,70 @@ function commandsModule({
}
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 }) => {
if (!displaySet) {
return;
@ -328,6 +392,8 @@ function commandsModule({
type,
});
},
/** Stores the changed position presentation */
updateStoredPositionPresentation: ({
viewportId,
displaySetInstanceUIDs,
@ -351,14 +417,15 @@ function commandsModule({
([key, value]) => {
return (
displaySetInstanceUIDs.every(uid => key.includes(uid)) &&
value.viewportId === viewportId
value?.viewportId === viewportId
);
}
)?.[0];
}
// Create presentation data with referencedImageId and options if provided
const presentationData = referencedImageId
const presentationData =
referencedImageId || options?.FrameOfReferenceUID
? {
...presentations.positionPresentation,
viewReference: {
@ -2153,9 +2220,7 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
jumpToMeasurement: {
commandFn: actions.jumpToMeasurement,
},
jumpToMeasurement: actions.jumpToMeasurement,
removeMeasurement: {
commandFn: actions.removeMeasurement,
},
@ -2415,6 +2480,7 @@ function commandsModule({
startRecordingForAnnotationGroup: actions.startRecordingForAnnotationGroup,
endRecordingForAnnotationGroup: actions.endRecordingForAnnotationGroup,
toggleSegmentLabel: actions.toggleSegmentLabel,
jumpToMeasurementViewport: actions.jumpToMeasurementViewport,
initializeSegmentLabelTool: actions.initializeSegmentLabelTool,
};

View File

@ -24,7 +24,9 @@ export const groupByStudy = (items, grouping, childProps) => {
let firstSelected, firstGroup;
items.forEach(item => {
items
.filter(item => item.displaySetInstanceUID)
.forEach(item => {
const studyUID = getItemStudyInstanceUID(item);
if (!groups.has(studyUID)) {
const items = [];

View File

@ -34,15 +34,20 @@ export default {
if (!viewportId) {
return [];
}
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetInsaneUIDs) {
if (!displaySetInstanceUIDs) {
return [];
}
const displaySets = displaySetInsaneUIDs.map(uid =>
displaySetService.getDisplaySetByUID(uid)
);
const displaySets = displaySetInstanceUIDs.map(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
.map(hp => {

View File

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

View File

@ -195,7 +195,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
{
name: 'cornerstone',
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);
initStudyPrefetcherService(servicesManager);
[
measurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
measurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
].forEach(event => {
measurementService.subscribe(event, evt => {
measurementService.subscribe(measurementService.EVENTS.JUMP_TO_MEASUREMENT, evt => {
const { measurement } = evt;
const { uid: annotationUID } = measurement;
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
});
commandsManager.runCommand('jumpToMeasurementViewport', { measurement, annotationUID, evt });
});
// When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,

View File

@ -436,7 +436,7 @@ const connectMeasurementServiceToTools = ({
return;
}
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID } = measurement;
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID, metadata } = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
@ -463,8 +463,12 @@ const connectMeasurementServiceToTools = ({
annotationUID: measurement.uid,
highlighted: 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,
toolName: measurement.toolName,
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
referencedImageId: imageId,

View File

@ -36,7 +36,6 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import { VOLUME_LOADER_SCHEME } from '../../constants';
const EVENTS = {
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;
export const WITH_NAVIGATION = { withNavigation: true, withOrientation: false };
export const WITH_ORIENTATION = { withNavigation: true, withOrientation: true };
/**
* Handles cornerstone viewport logic including enabling, disabling, and
@ -551,10 +551,13 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
* was initiated.
* @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
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;
}
@ -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
// the active viewport could change orientations to show this
if (
activeViewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
activeViewport?.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
) {
return activeViewportId;
}
@ -589,6 +592,86 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
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.
*/
@ -626,7 +709,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// is being used to navigate to the initial view position for measurement
// navigation and other navigation forcing specific views.
let initialImageIndexToUse =
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
presentations?.positionPresentation?.initialImageIndex ?? <number>initialImageIndex;
const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions();
@ -662,7 +745,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
initialImageIndexToUse = imageIds.indexOf(referencedImageId);
}
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
if (
initialImageIndexToUse === undefined ||
initialImageIndexToUse === null ||
initialImageIndexToUse < 0
) {
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
}
@ -1214,7 +1301,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
): void {
const viewRef = positionPresentation?.viewReference;
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);
} else {
console.warn('Unable to apply reference viewable', viewRef);

View File

@ -3,20 +3,17 @@ import { Enums } from '@cornerstonejs/core';
import OrientationAxis = Enums.OrientationAxis;
export const isReferenceViewable = ({
viewportId,
reference,
viewportOptions,
servicesManager,
}) => {
export const isReferenceViewable = (servicesManager, viewportId, reference, viewportOptions?) => {
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
if (!viewportOptions) {
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, {
withNavigation: true,
asVolume: true,
});
return isViewable;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,36 @@
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
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
@ -13,11 +44,7 @@ export default function getSOPInstanceAttributes(imageId, displaySetService, ann
}
const { metadata } = annotation;
const { volumeId } = metadata;
const displaySet = displaySetService.getDisplaySetsBy(displaySet =>
volumeId.includes(displaySet.uid)
)[0];
const displaySet = getDisplaySet({ metadata, displaySetService });
const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
return {

View File

@ -36,7 +36,7 @@
"peerDependencies": {
"@ohif/core": "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",
"prop-types": "^15.6.2",
"react": "^18.3.1",

View File

@ -22,7 +22,7 @@ const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
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 metadataProvider = classes.MetadataProvider;

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",

View File

@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",

View File

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

View File

@ -21,7 +21,16 @@ const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = '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
// ends up occurring before the Viewport is created, so the displaySet
// is not loaded yet, and isRehydratable is undefined unless we call load().
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
!displaySet.isLoaded &&
displaySet.load
) {
if (hasValidSOPClassHandlerId(displaySet) && !displaySet.isLoaded && displaySet.load) {
await displaySet.load();
}
// Magic string
// load function added by our sopClassHandler module
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
hasValidSOPClassHandlerId(displaySet) &&
displaySet.isRehydratable === true &&
!displaySet.isHydrated
) {

View File

@ -29,7 +29,7 @@ function getViewportModule({ servicesManager, commandsManager, extensionManager
{
name: 'cornerstone-tracked',
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 { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal';
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
utils.MeasurementFilters;
const { filterMeasurementsBySeriesUID, filterAny } = utils.MeasurementFilters;
function PanelMeasurementTableTracking(props) {
const [viewportGrid] = useViewportGrid();
@ -23,9 +22,7 @@ function PanelMeasurementTableTracking(props) {
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const measurementFilter = trackedStudy
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
: filterPlanarMeasurement;
const measurementFilter = trackedStudy ? filterMeasurementsBySeriesUID(trackedSeries) : filterAny;
const onUntrackConfirm = () => {
sendTrackedMeasurementsEvent('UNTRACK_ALL', {});

View File

@ -35,25 +35,10 @@ function TrackedCornerstoneViewport(
const { SeriesInstanceUID } = displaySet;
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) {
setIsTracked(!isTracked);
}
}, [isTracked, trackedMeasurements, viewportId, SeriesInstanceUID]);
}, [isTracked, SeriesInstanceUID, trackedSeries]);
const onElementEnabled = useCallback(
evt => {

View File

@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",

View File

@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "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",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",

View File

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

View File

@ -27,6 +27,7 @@ const tracked = {
const dicomsr = {
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',
};
@ -251,7 +252,7 @@ function modeFactory() {
},
{
namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler],
displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D],
},
{
namespace: dicomvideo.viewport,
@ -284,6 +285,7 @@ function modeFactory() {
ohif.sopClassHandler,
dicompdf.sopClassHandler,
dicomsr.sopClassHandler,
dicomsr.sopClassHandler3D,
],
hotkeys: {
name: 'basic-test-hotkeys',

View File

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

View File

@ -34,8 +34,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"@ohif/core": "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",

View File

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

View File

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

View File

@ -26,7 +26,7 @@ window.config = {
// filterQueryParam: false,
// Uses the ohif datasource as the default - this requires that KHEOPS be
// 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 */
// dangerouslyUseDynamicConfig: {
// enabled: true,
@ -40,7 +40,7 @@ window.config = {
dataSources: [
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb',
sourceName: 'ohif',
configuration: {
friendlyName: 'AWS S3 Static wado server',
name: 'aws',
@ -54,47 +54,15 @@ window.config = {
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)
singlepart: 'video,pdf',
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
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',
sourceName: 'ohif3',

View File

@ -1,5 +1,5 @@
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 { useViewportGrid } from '@ohif/ui-next';
import EmptyViewport from './EmptyViewport';
@ -14,13 +14,8 @@ function ViewerViewportGrid(props: withAppTypes) {
const { numCols, numRows } = layout;
const layoutHash = useRef(null);
const {
displaySetService,
measurementService,
hangingProtocolService,
uiNotificationService,
customizationService,
} = servicesManager.services;
const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
servicesManager.services;
const generateLayoutHash = () => `${numCols}-${numRows}`;
@ -32,9 +27,9 @@ function ViewerViewportGrid(props: withAppTypes) {
*/
const updateDisplaySetsFromProtocol = (
protocol: Types.HangingProtocol.Protocol,
_protocol: Types.HangingProtocol.Protocol,
stage,
activeStudyUID,
_activeStudyUID,
viewportMatchDetails
) => {
const availableDisplaySets = displaySetService.getActiveDisplaySets();
@ -154,100 +149,6 @@ function ViewerViewportGrid(props: withAppTypes) {
}
}, [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 { viewportGridService } = servicesManager.services;
const customOnDropHandler = customizationService.getCustomization('customOnDropHandler');
@ -267,23 +168,6 @@ function ViewerViewportGrid(props: withAppTypes) {
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 viewportPanes = [];
@ -315,27 +199,12 @@ function ViewerViewportGrid(props: withAppTypes) {
return !displaySet?.unsupported;
});
const { component: ViewportComponent, isReferenceViewable } = _getViewportComponent(
const { component: ViewportComponent } = _getViewportComponent(
displaySets,
viewportComponents,
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
const displaySetsNeedsRerendering = displaySets.some(displaySet => {
return displaySet.needsRerendering;
@ -462,8 +331,8 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
throw new Error('displaySetsToDisplay is null');
}
if (viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)) {
const { component, isReferenceViewable } = viewportComponents[i];
return { component, isReferenceViewable };
const { component } = viewportComponents[i];
return { component };
}
}
@ -474,7 +343,7 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
type: 'error',
});
return { component: EmptyViewport, isReferenceViewable: () => false };
return { component: EmptyViewport };
}
export default ViewerViewportGrid;

View File

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

View File

@ -74,10 +74,11 @@ const EVENTS = {
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed',
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
// Give the viewport a chance to jump to the measurement
JUMP_TO_MEASUREMENT_VIEWPORT: 'event:jump_to_measurement_viewport',
// Give the layout a chance to jump to the measurement
JUMP_TO_MEASUREMENT_LAYOUT: 'event:jump_to_measurement_layout',
/**
* Indicate some viewport should be jumped to. This will have to be implemented
* by a single handler that can look at all viewports to decide who should handle it.
*/
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
};
const VALUE_TYPES = {
@ -431,11 +432,7 @@ class MeasurementService extends PubSubService {
return;
}
let internalUID = data.id;
if (!internalUID) {
internalUID = guid();
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
}
const internalUID = data.uid || guid();
const annotationData = data.annotation.data;
@ -497,7 +494,7 @@ class MeasurementService extends PubSubService {
mapping => mapping.annotationType === annotationType
);
if (!sourceMapping) {
console.log('No source mapping', source);
console.log('No source mapping', source.uid, annotationType, source);
return;
}
const { toMeasurementSchema } = sourceMapping;
@ -656,19 +653,7 @@ class MeasurementService extends PubSubService {
}
/**
* This method calls the subscriptions for JUMP_TO_MEASUREMENT_VIEWPORT
* 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.
* This method calls the subscription for JUMP_TO_MEASUREMENT
*/
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.`);
return;
}
const consumableEvent = this.createConsumableEvent({
const event = {
viewportId,
measurement,
});
};
// Important: we should broadcast the layout event first, since
// in the layout there might be a viewport that we can match and choose
// and jump in it before we decide on changing the orientation of different viewports
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, consumableEvent);
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, consumableEvent);
// A single handler will decide on which window to jump to, previously
// this was handled by a consumable event
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT, event);
}
_getSourceUID(name, version) {

View File

@ -45,10 +45,6 @@ class ViewportGridService extends PubSubService {
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 {
const state = this.getState();
const viewport = state.viewports.get(viewportId);
@ -104,7 +100,6 @@ class ViewportGridService extends PubSubService {
set: setImplementation,
getNumViewportPanes: getNumViewportPanesImplementation,
setViewportIsReady: setViewportIsReadyImplementation,
setIsReferenceViewable: setIsReferenceViewableImplementation,
getViewportState: getViewportStateImplementation,
}): void {
if (getViewportStateImplementation) {
@ -139,9 +134,6 @@ class ViewportGridService extends PubSubService {
if (setViewportIsReadyImplementation) {
this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation;
}
if (setIsReferenceViewableImplementation) {
this.serviceImplementation._setIsReferenceViewable = setIsReferenceViewableImplementation;
}
}
public publishViewportsReady() {

View File

@ -29,6 +29,9 @@ export type DisplaySet = {
/** A fetch method to get the thumbnail */
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
* pdf display.

View File

@ -45,10 +45,6 @@ export interface ViewportGridState {
activeViewportId: string | null;
layout: Layout;
isHangingProtocolLayout: boolean;
isReferenceViewable: (props: {
viewportId: string;
reference: Record<string, unknown>;
}) => boolean;
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.
* **`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:**

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) |
| MEASUREMENT_REMOVED | Fires when a measurement is removed |
| 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_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. |
| JUMP_TO_MEASUREMENT | Fires when a measurement is requested to be jumped to |
## API

View File

@ -246,11 +246,9 @@ const DefaultFallback = ({
const ErrorBoundary = ({
context = 'OHIF',
onReset = () => {},
onError = () => {},
onError = _error => {},
fallbackComponent: FallbackComponent = DefaultFallback,
children,
fallbackRoute = null,
isPage,
}: ErrorBoundaryProps) => {
const [error, setError] = useState<ErrorBoundaryError | null>(null);
@ -276,8 +274,8 @@ const ErrorBoundary = ({
event.preventDefault();
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
setError(event.reason);
onErrorHandler(event.reason, null);
setError(event.reason || event);
onErrorHandler(event.reason || event, null);
}, 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);
onError(error, componentStack || '', context);
};

View File

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

View File

@ -25,6 +25,7 @@ const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO;
// Add port constant
const OHIF_PORT = Number(process.env.OHIF_PORT || 3000);
const OHIF_OPEN = process.env.OHIF_OPEN !== 'false';
export default defineConfig({
source: {
@ -142,7 +143,7 @@ export default defineConfig({
},
server: {
port: OHIF_PORT,
open: true,
open: OHIF_OPEN,
// Configure proxy
proxy: {
'/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.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.
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.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.
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.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.
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.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.
assertNumberOfModalityLoadBadges({ page, expectedCount: 0 });
await page.waitForTimeout(5000);
await checkForScreenshot({
page,
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('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.segNoHydrationThenMPR.segNoHydrationPreMPR);
await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click();
await page.waitForTimeout(5000);
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"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.30.3.tgz#f1b0bac426e2a0916f2f971677291e4c4b3ac4bc"
integrity sha512-rlppvJBjty9fkoFpWdRoPYKB2CGtCp+AiTQBk1OnM181Ok7r+NFEoe7e1PARHjbMjrH/Y5vOLnqEon/ABdbm8A==
"@cornerstonejs/adapters@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-3.32.5.tgz#bd292827868267d93de1f94b20545f079f09ed1e"
integrity sha512-tpd+sHjE54VOqme83HZ9XqGKztjikrwbrWR4M3e8X+fuR6LJ+qNQ6+YkPdfThzu0KxN+q8mO5aSvGyJo82rRnA==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3"
dcmjs "^0.42.0"
dcmjs "^0.43.1"
gl-matrix "^3.4.3"
ndarray "^1.0.19"
"@cornerstonejs/ai@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.30.3.tgz#88c3a678df1ba06f80010ebfaa98851c80ec43ea"
integrity sha512-KNz/FLhoBZFJkrlMIfmS7PCSwBH9YEcg5Sh0HKw3GxpV6XE1H9tKl2c5oH/DHuJFj8gMC2FRnH6rOx3x+BpOrQ==
"@cornerstonejs/ai@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/ai/-/ai-3.32.5.tgz#6ca644702f034a692f8a309c10c2118e25f0382a"
integrity sha512-j7paST1ounIAvJYS9/qqOsjknjdFcfcNdwCKiwB5KpobL2/BmYNl4kEZNdYXZPhFfQPQvgpW71OedxkDdwaJ2A==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
buffer "^6.0.3"
dcmjs "^0.42.0"
dcmjs "^0.43.1"
gl-matrix "^3.4.3"
lodash.clonedeep "^4.5.0"
ndarray "^1.0.19"
@ -1369,30 +1369,20 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.7.tgz#784394d05d8cf735640e9c3baa206535ee8e0376"
integrity sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w==
"@cornerstonejs/core@^3.16.0":
version "3.24.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.24.0.tgz#6f184b5eedbf5cc2266bb899cf9454cd866b4e4e"
integrity sha512-Ep5hVrM5XMVuExL2i8tiVuFGqoi1QyaZpU3WzxqhH75nNpckSi/ZYqic4rI3qb/V4IKYv2R+8Gp4Jptl110u4A==
"@cornerstonejs/core@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.32.5.tgz#527cffa6e48810fd2d141fefa714ff6886368b4f"
integrity sha512-6m/ODfSyM+8/ZigD2wM2wl8yjxmDx9DTen/R5nxCHUe4sqK97PKjfmcN5OK9cOCzwlzqQBZTFow1Ra6UAfkKCA==
dependencies:
"@kitware/vtk.js" "32.12.1"
comlink "^4.4.1"
gl-matrix "^3.4.3"
loglevel "^1.9.2"
"@cornerstonejs/core@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-3.30.3.tgz#2f7805c11a4344f21dc1f7a1918a3c7430180802"
integrity sha512-nGfMNpL4VnPnYTDKBWgDxLpWLl/E3QUoEtScZDN5rKSQrocO/WgX2pt2vxBjbfI5NVPtngR3ly/qffg13EeOaw==
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==
"@cornerstonejs/dicom-image-loader@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-3.32.5.tgz#b91bf19b31a307b03766b154f39eabd58caafd36"
integrity sha512-fOUkphjdzTG8Flw0XV4bGUEZtbASeE2/Y+HssKr4PiwiDvGcVvVQF7RxyKkvabP3OsZIAaBOhPB8ghKEEXDp2w==
dependencies:
"@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
@ -1404,34 +1394,25 @@
pako "^2.0.4"
uuid "^9.0.0"
"@cornerstonejs/labelmap-interpolation@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.30.3.tgz#475f116dce301df4cefdca6728414438b9f1fa12"
integrity sha512-WVPqidTTO9mWaSir0uERHqRmXAOl9CzP5EhAumoR/sZaQv0ZUDhkyOjc8jv8MZftSTRLSnlr0Ovy/Tnfxwl45w==
"@cornerstonejs/labelmap-interpolation@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/labelmap-interpolation/-/labelmap-interpolation-3.32.5.tgz#d2808aba8a528bd63522c4759f445ea2603f3686"
integrity sha512-+E476+jgAAOGUVUcdMD0D2K5/yHfc3rCVCgApfZ9H5mOaEavVHPjGxomFkE5twnBfQIBKgTos3968anl21ziAw==
dependencies:
"@itk-wasm/morphological-contour-interpolation" "1.1.0"
itk-wasm "1.0.0-b.165"
"@cornerstonejs/polymorphic-segmentation@^3.30.3":
version "3.30.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.30.3.tgz#b8fdab95df95af7d83b125ce74942fed4c239eaf"
integrity sha512-5LWPRroG6fYdk/CmTKUYyop1M6BUAT5qrXWvMD3xsWMKMW5ScQohRoPZX4hLIfxuhb/ckuiiQwNqixgcDOH2Gw==
"@cornerstonejs/polymorphic-segmentation@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/polymorphic-segmentation/-/polymorphic-segmentation-3.32.5.tgz#edfbd9bd7c3060628f0b8f38a9abb1380087a449"
integrity sha512-MyG1ULFhOEdc8ud+5NJhVUmlBis61K3IWod1UYxZM4VdY7kivbdPbWXaynhnYTUg5OeXGPsETLWdLw/5E32mag==
dependencies:
"@icr/polyseg-wasm" "0.4.0"
"@cornerstonejs/tools@^3.16.0":
version "3.24.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.24.0.tgz#7e4bb8a8e8a982316e8f53282441f888f26bb70b"
integrity sha512-KYkElmvmFwuskKnp9glUIwiHYyVfr1LCqOjhj1y1+rgzxAqlrfaGmS7y8KHOqGHzQwXMMoaEKzRGRviAS915tg==
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==
"@cornerstonejs/tools@^3.32.5":
version "3.32.5"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-3.32.5.tgz#1b6fa662a862332fc220c4899fab4303abfd4088"
integrity sha512-h1UWy/u8Cz1QPsMvbNF+rRGwaEdRnGwCbOSZQwLzYOuvaV+rvGAI6l+Xbhe9zUgzE1lSbBigiQjdGHiNtnVWPQ==
dependencies:
"@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1"
@ -8745,23 +8726,10 @@ dayjs@^1.10.4:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
dcmjs@^0.29.8:
version "0.29.13"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.13.tgz#a74df541bf3948366b25630dbe1e8376f108ea3b"
integrity sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA==
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==
dcmjs@0.43.1, dcmjs@^0.29.8, dcmjs@^0.43.1:
version "0.43.1"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.43.1.tgz#03223e56d52e9ec2c3b8c10afe9200d754befe62"
integrity sha512-7IAB2cs2F8QYINbfhA7PV+IDraqBuHn8N31xJc6HsyNxZXyGUPQRujcq732ACqjLhl7oJi1z8PJJgLdblQtD3Q==
dependencies:
"@babel/runtime-corejs3" "^7.22.5"
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"
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:
version "5.3.2"
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"
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"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -19691,6 +19654,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.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:
version "2.1.1"
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-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"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -19820,6 +19792,13 @@ strip-ansi@^4.0.0:
dependencies:
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:
version "7.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
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"
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:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"