fix(SR): Added support for spline and live wire SR items. (#5870)

* fix(SR): Added support for spline and live wire SR items.

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add a script to checkout a worktree for test builds

* fix: Allow download for testing sr validator

* Remove script that wasn't intended to be included

* Bump CS3D version.

* PR comments - simplify code and use single codepath for download

* Allow both download and save buttons for SEG and RTSTRUCT

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-03-17 08:59:47 -04:00
1 parent 69d8ad63f3
commit 1d4802c2a3
13 files changed
+220 -160

No files matched your search

@@ -1,14 +1,11 @@
import { metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
import dcmjs from 'dcmjs';
import OHIF from '@ohif/core';
import { adaptersSR } from '@cornerstonejs/adapters';
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
const { downloadBlob } = utils;
const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { log } = OHIF;
@@ -75,24 +72,8 @@ const commandsModule = (props: withAppTypes) => {
/**
*
* @param measurementData An array of measurements from the measurements service
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
* as opposed to Finding Sites.
* that you wish to serialize.
*/
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
const srDataset = _generateReport(measurementData, additionalFindingTypes, options);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
downloadBlob(reportBlob, { filename: 'dicom-sr.dcm' });
},
/**
*
* @param measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param dataSource The dataSource that you wish to use to persist the data.
* @param dataSource The data source name ('download', 'copyToClipboard', or a named data source).
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
* @return The naturalized report
@@ -103,23 +84,30 @@ const commandsModule = (props: withAppTypes) => {
additionalFindingTypes,
options = {},
}) => {
// Use the @cornerstonejs adapter for converting to/from DICOM
// But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
const storeFn = commandsManager.runCommand('createStoreFunction', {
dataSource,
defaultFileName: 'dicom-sr.dcm',
});
if (!storeFn) {
log.error('[DICOMSR] No valid store for dataSource:', dataSource);
return Promise.reject({});
}
try {
const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options);
const naturalizedReport = _generateReport(
measurementData,
additionalFindingTypes,
options
);
const { StudyInstanceUID, ContentSequence } = naturalizedReport;
const { ContentSequence } = naturalizedReport;
// The content sequence has 5 or more elements, of which
// the `[4]` element contains the annotation data, so this is
// checking that there is some annotation data present.
if (!ContentSequence?.[4].ContentSequence?.length) {
if (!ContentSequence?.[4]?.ContentSequence?.length) {
console.log('naturalizedReport missing imaging content', naturalizedReport);
throw new Error('Invalid report, no content');
}
@@ -128,22 +116,12 @@ const commandsModule = (props: withAppTypes) => {
}
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
let dicomDict;
if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
}
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
}
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
DicomMetadataStore.addInstances([naturalizedReport], true);
await storeFn(naturalizedReport, { measurementData, dicomDict });
return naturalizedReport;
} catch (error) {
@@ -166,7 +144,6 @@ const commandsModule = (props: withAppTypes) => {
};
const definitions = {
downloadReport: actions.downloadReport,
storeMeasurements: actions.storeMeasurements,
hydrateStructuredReport: actions.hydrateStructuredReport,
};
@@ -637,16 +637,35 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
// Handle spatial reference ONLY if ContentSequence exists
// Handle spatial reference ONLY if ContentSequence exists.
// ContentSequence may be a scalar SCOORD or an array when additional named
// SCOORDs (e.g. control points) are nested alongside the primary geometry.
// Pick the primary geometry entry: prefer the SCOORD without a
// ConceptNameCodeSequence (plain polyline), falling back to the first SCOORD.
if (ContentSequence) {
const { ValueType } = ContentSequence;
const scoordItem = Array.isArray(ContentSequence)
? (ContentSequence.find(
cs =>
(cs.ValueType === 'SCOORD' || cs.ValueType === 'SCOORD3D') &&
!cs.ConceptNameCodeSequence
) ?? ContentSequence.find(cs => cs.ValueType === 'SCOORD' || cs.ValueType === 'SCOORD3D'))
: ContentSequence;
if (!scoordItem) {
console.warn(
'ContentSequence array contains no SCOORD or SCOORD3D entry, skipping annotation.'
);
return;
}
const { ValueType } = scoordItem;
if (ValueType !== 'SCOORD' && ValueType !== 'SCOORD3D') {
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
return;
}
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
const coords = _getCoordsFromSCOORDOrSCOORD3D(scoordItem);
if (coords) {
measurement.coords.push(coords);