I am using Cornerstone3D v1 (@cornerstonejs/core, @cornerstonejs/tools) in a React application.
Currently, I have successfully implemented 2D stack and volume (MPR-style) viewports for CT/MR studies using cornerstoneStreamingImageVolumeLoader.
I can:
- Load DICOM series from a WADO-RS endpoint
- Create volumes using
createAndCacheVolume - Convert stack viewports to volume viewports using
convertStackToVolumeViewport - Scroll through slices and use tools (StackScroll, Crosshairs, ROI, etc.)
Now I want to implement true 3D construction / 3D volume rendering (VR) (e.g. shaded 3D view of CT data), but I’m facing difficulty understanding:
import * as cornerstone from ‘@cornerstonejs/core’;
import * as cornerstoneTools from ‘@cornerstonejs/tools’;
import cornerstoneDICOMImageLoader from ‘@cornerstonejs/dicom-image-loader’;
import { cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader } from ‘@cornerstonejs/streaming-image-volume-loader’;
import dicomParser from ‘dicom-parser’;
import createImageIdsAndCacheMetaData from ‘…/…/utils/createImageIdsAndCacheMetaData’;
import { CKEditor } from “@ckeditor/ckeditor5-react”;
import DecoupledEditor from “@ckeditor/ckeditor5-build-decoupled-document”;
// Toolgroup and rendering setup
const toolGroupId = “myToolGroup”;
const renderingEngineId = “myRenderingEngine”;
const viewportIds = [‘first’, ‘second’, ‘third’, ‘fourth’];
const indexMap = {first: ‘viewport1Index’}
class ClinetViwer extends Component {
constructor(props) {
super(props);
this.state = {
};
allowDrop(event) {
event.preventDefault();
}
async openImageInViewport(ID, modality, targetViewportId) {
try {
let viewport = this.renderingEngine.getViewport(targetViewportId);
// If viewport doesn't exist, enable it first
if (!viewport) {
const viewportConfig = this.viewport_list[targetViewportId];
if (viewportConfig) {
this.renderingEngine.enableElement(viewportConfig);
viewport = this.renderingEngine.getViewport(targetViewportId);
} else {
console.error(`Viewport ${targetViewportId} not found in viewport_list`);
return;
}
}
if (!viewport) {
console.error(`Viewport ${targetViewportId} is not properly initialized`);
return;
}
if (viewport.type === cornerstone.Enums.ViewportType.VOLUME) {
viewport.setVolumes([]);
}
if (modality === 'CT' || modality === 'MR') {
const volume = cornerstone.cache.getVolume(ID);
if (!volume) {
console.error(`Volume ${ID} not found in cache`);
return;
}
const hasMultipleSlices = volume && volume.imageIds.length > 1;
if (hasMultipleSlices) {
const newViewport = await cornerstone.utilities.convertStackToVolumeViewport({
options: { volumeId: ID, viewportId: targetViewportId, orientation: cornerstone.Enums.OrientationAxis.ACQUISITION },
viewport: viewport,
});
newViewport.setProperties({ rotation: 0 });
if (this.toolGroup) {
try {
this.toolGroup.addViewport(newViewport.id, renderingEngineId);
} catch (e) {
// Ignore if already exists
}
}
newViewport.render();
const newImageListener = () => {
const index = newViewport.getSliceIndex() + 1;
const indexElement = document.getElementById(indexMap[newViewport.id]);
if (indexElement) {
indexElement.innerHTML = 'Image: ' + index;
}
};
newViewport.element.addEventListener(cornerstone.EVENTS.VOLUME_NEW_IMAGE, newImageListener);
this.eventListeners.push({element: newViewport.element, type: cornerstone.EVENTS.VOLUME_NEW_IMAGE, listener: newImageListener});
} else {
viewport.setStack(volume.imageIds);
viewport.render();
}
} else {
if (viewport.type === cornerstone.Enums.ViewportType.STACK) {
const imageIds = this.nonCT_ImageIds[Number(ID)];
if (imageIds) {
viewport.setStack(imageIds);
viewport.render();
} else {
console.error(`Non-CT image IDs not found for ID: ${ID}`);
}
} else {
this.renderingEngine.disableElement(targetViewportId);
let curr = this.viewport_list[targetViewportId];
if (curr) {
curr.type = cornerstone.Enums.ViewportType.STACK;
delete curr.defaultOptions;
this.renderingEngine.enableElement(curr);
const newViewport = this.renderingEngine.getViewport(targetViewportId);
if (newViewport) {
newViewport.setProperties({ rotation: 0 });
if (this.toolGroup) {
try {
this.toolGroup.addViewport(newViewport.id, renderingEngineId);
} catch (e) {
// Ignore if already exists
}
}
const imageIds = this.nonCT_ImageIds[Number(ID)];
if (imageIds) {
newViewport.setStack(imageIds);
newViewport.render();
const newImageListener = () => {
const index = newViewport.getCurrentImageIdIndex() + 1;
const indexElement = document.getElementById(indexMap[newViewport.id]);
if (indexElement) {
indexElement.innerHTML = 'Image: ' + index;
}
};
newViewport.element.addEventListener(cornerstone.EVENTS.STACK_NEW_IMAGE, newImageListener);
this.eventListeners.push({element: newViewport.element, type: cornerstone.EVENTS.STACK_NEW_IMAGE, listener: newImageListener});
} else {
console.error(`Non-CT image IDs not found for ID: ${ID}`);
}
}
} else {
console.error(`Viewport configuration not found for: ${targetViewportId}`);
}
}
}
} catch (error) {
console.error("Error opening image in viewport:", error);
}
}
drop(event) {
event.preventDefault();
// Get dropped item info
const obj = JSON.parse(event.dataTransfer.getData('text'));
const ID = obj[0];
const modality = obj[1];
// Get current viewport
const parentElement = event.target.parentElement;
const viewport_ID = parentElement.getAttribute('data-value');
// Force a small delay to let React finish its updates
setTimeout(() => {
this.openImageInViewport(ID, modality, viewport_ID);
}, 10);
}
async cornerstone(PARAM) {
try {
const previewTab = document.getElementById(‘previewTab’);
if (!previewTab) {
console.error(“Preview tab element not found”);
return;
}
const studyid = PARAM;
// Create a loading message div
const loadingMessage = document.createElement('div');
loadingMessage.id = 'loadingMessage';
loadingMessage.innerText = 'Please wait, images are loading...';
loadingMessage.style.position = 'absolute';
loadingMessage.style.top = '50%';
loadingMessage.style.left = '50%';
loadingMessage.style.transform = 'translate(-50%, -50%)';
loadingMessage.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
loadingMessage.style.color = '#fff';
loadingMessage.style.padding = '10px 20px';
loadingMessage.style.borderRadius = '5px';
loadingMessage.style.fontSize = '16px';
loadingMessage.style.zIndex = '1000';
loadingMessage.style.pointerEvents = 'none';
document.body.appendChild(loadingMessage);
// Timeout for the loading message - 1 minute
const loadingMessageTimeout = setTimeout(() => {
const message = document.getElementById('loadingMessage');
if (message) {
document.body.removeChild(message);
}
}, 60000);
// Use the patient data from state instead of fetching again
const { patientData } = this.state;
if (!patientData) {
throw new Error('Patient data not available');
}
const { study_instance_uid } = patientData;
// Since we don't have series data from the new API, we'll need to adapt
// For now, let's assume we need to fetch series data separately or modify the API
// This part needs to be adjusted based on your actual data structure
const response = await fetch(`${API_BASE_URL}/serverdata/`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `studyid=${encodeURIComponent(studyid)}`
});
if (!response.ok) throw new Error(`Server error: ${response.status}`);
const data = await response.json();
this.setState({ studyData: data });
const { study_uid, series } = data;
let k = 0;
let imageIdIndex = 0;
let loadedSeriesCount = 0;
// Handle each series asynchronously
const imagePromises = series.map(async (item, index) => {
const startTime = Date.now();
let imageId = await createImageIdsAndCacheMetaData({
StudyInstanceUID: study_uid,
SeriesInstanceUID: item[0],
wadoRsRoot: '*******',
});
let imageCount = 0;
if (Array.isArray(imageId)) {
imageCount = imageId.length;
}
// Create wrapper container (card for image + text)
const cardContainer = document.createElement('div');
cardContainer.style.display = 'flex';
cardContainer.style.alignItems = 'center';
cardContainer.style.border = '2px solid #ef4444';
cardContainer.style.borderRadius = '6px';
cardContainer.style.padding = '6px';
cardContainer.style.marginBottom = '8px';
cardContainer.style.backgroundColor = '#1D1D1F';
cardContainer.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
cardContainer.style.cursor = 'pointer';
// Create image
let image = document.createElement('img');
image.src = item[3];
image.style.height = '80px';
image.style.width = '100px';
image.style.borderRadius = '4px';
image.style.border = '1px solid #444';
image.style.marginRight = '10px';
// Create text container (common for both CT/MR and non-CT)
const textContainer = document.createElement('div');
textContainer.style.flex = '1';
textContainer.style.color = '#f1f1f1';
textContainer.style.fontSize = '11px';
textContainer.style.lineHeight = '1.4';
textContainer.innerHTML = `
<p style="margin:0;">
<strong>${item[1]}</strong>
</p>
<p style="margin:0;">${item[2]}</p>
<p style="margin:2px 0; font-size:10px; color:#bbb;">
Image Count: <strong>${imageCount}</strong>
</p>
`;
// Process based on modality type
if ((item[1] === 'CT' || item[1] === 'MR') && Array.isArray(imageId) && imageId.length > 0) {
// Patch metadata for all frames to avoid vec3 errors
imageId.forEach((id) => {
const imagePlaneMeta = cornerstone.metaData.get(‘imagePlaneModule’, id);
if (!imagePlaneMeta || !Array.isArray(imagePlaneMeta.imageOrientationPatient)) {
cornerstone.metaData.addProvider((type, metaId) => {
if (type !== 'imagePlaneModule' || metaId !== id) return undefined;
return {
...imagePlaneMeta,
imageOrientationPatient: imagePlaneMeta?.imageOrientationPatient || [1, 0, 0, 0, 1, 0],
imagePositionPatient: imagePlaneMeta?.imagePositionPatient || [0, 0, 0],
rowCosines: [1, 0, 0],
columnCosines: [0, 1, 0],
frameOfReferenceUID: imagePlaneMeta?.frameOfReferenceUID || '1.2.840.10008.1.2.1.999999',
rows: imagePlaneMeta?.rows || 512,
columns: imagePlaneMeta?.columns || 512,
pixelSpacing: imagePlaneMeta?.pixelSpacing || [1.0, 1.0],
};
}, 1000);
}
});
// Assign a unique volumeId
let volumeId = ‘cornerstoneStreamingImageVolume: myVolume’ + k;
k += 1;
image.dataset.value = volumeId;
try {
// Create and cache the volume safely
let volume = await cornerstone.volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageId });
cornerstone.utilities.cacheUtils.performCacheOptimizationForVolume(volumeId);
volume.load();
} catch (err) {
console.error(‘Error creating volume for series:’, item, err);
return; // skip this series if volume creation fails
}
image.dataset.modality = item[1];
image.dataset.description = item[2];
image.draggable = true;
image.addEventListener(‘load’, () => {
loadedSeriesCount++;
});
} else {
// Non-CT/MR processing
this.nonCT_ImageIds.push(imageId);
image.dataset.value = imageIdIndex;
imageIdIndex += 1;
image.dataset.modality = item[1];
image.dataset.description = item[2];
image.draggable = true;
image.addEventListener('load', () => {
loadedSeriesCount++;
});
}
// Add click event listener (common for both types)
const clickListener = (event) => {
const ID = event.target.dataset.value;
const modality = event.target.dataset.modality;
const targetViewportId = this.selected_viewport;
this.openImageInViewport(ID, modality, targetViewportId);
};
image.addEventListener('click', clickListener);
this.eventListeners.push({element: image, type: 'click', listener: clickListener});
// Append children to card container (ONCE for both types)
cardContainer.appendChild(image);
cardContainer.appendChild(textContainer);
// Add the card to preview tab (SINGLE APPEND POINT)
previewTab.appendChild(cardContainer);
const endTime = Date.now();
const elapsedTime = endTime - startTime;
console.log(`Series ${index + 1} (modality: ${item[1]}): Load Time = ${elapsedTime} ms`);
return item;
});
// Once all promises are resolved, proceed to cleanup
Promise.all(imagePromises)
.then(() => {
clearTimeout(loadingMessageTimeout);
const message = document.getElementById('loadingMessage');
if (message) {
document.body.removeChild(message);
}
const firstImage = previewTab.querySelector('img');
if (firstImage) {
const ID = firstImage.dataset.value;
const modality = firstImage.dataset.modality;
const targetViewportId = viewportIds[0];
this.openImageInViewport(ID, modality, targetViewportId);
}
})
.catch(error => {
clearTimeout(loadingMessageTimeout);
const message = document.getElementById('loadingMessage');
if (message) {
document.body.removeChild(message);
}
console.error('Error while loading images:', error);
});
const dragStartListener = (event) => {
if (event.target.tagName === 'IMG') {
const transferData = [event.target.dataset.value, event.target.dataset.modality, event.target.dataset.description];
event.dataTransfer.setData("text", JSON.stringify(transferData));
}
};
previewTab.addEventListener('dragstart', dragStartListener);
this.eventListeners.push({element: previewTab, type: 'dragstart', listener: dragStartListener});
} catch (error) {
console.error(error);
const message = document.getElementById('loadingMessage');
if (message) {
document.body.removeChild(message);
}
}
}
async initializeCornerstone() {
if (this.state.cornerstoneInitialized) return;
await cornerstone.init();
cornerstoneTools.init();
// Add metadata providers
cornerstone.metaData.addProvider(
cornerstone.utilities.calibratedPixelSpacingMetadataProvider.get.bind(
cornerstone.utilities.calibratedPixelSpacingMetadataProvider
),
11000
);
cornerstone.metaData.addProvider(
cornerstone.utilities.genericMetadataProvider.get.bind(
cornerstone.utilities.genericMetadataProvider
),
10000
);
const { preferSizeOverAccuracy, useNorm16Texture } =
cornerstone.getConfiguration().rendering;
window.cornerstone = cornerstone;
window.cornerstoneTools = cornerstoneTools;
// Register volume loaders
cornerstone.volumeLoader.registerVolumeLoader(
"cornerstoneStreamingImageVolume",
cornerstoneStreamingImageVolumeLoader
);
cornerstone.volumeLoader.registerUnknownVolumeLoader(
cornerstoneStreamingImageVolumeLoader
);
cornerstone.volumeLoader.registerVolumeLoader(
"cornerstoneStreamingDynamicImageVolume",
cornerstoneStreamingDynamicImageVolumeLoader
);
// Configure DICOM image loader
cornerstoneDICOMImageLoader.external.cornerstone = cornerstone;
cornerstoneDICOMImageLoader.external.dicomParser = dicomParser;
cornerstoneDICOMImageLoader.configure({
useWebWorkers: true,
decodeConfig: {
convertFloatPixelDataToInt: false,
use16BitDataType: preferSizeOverAccuracy || useNorm16Texture,
},
});
let maxWebWorkers = 1;
if (navigator.hardwareConcurrency) {
maxWebWorkers = Math.min(navigator.hardwareConcurrency, 10);
}
// Web worker settings
const config = {
maxWebWorkers,
startWebWorkersOnDemand: false,
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,
strict: false,
},
},
};
cornerstoneDICOMImageLoader.webWorkerManager.initialize(config);
this.setState({ cornerstoneInitialized: true });
}
async componentDidMount() {
// Fetch patient data first
const patientData = await this.fetchPatientData();
if (!patientData) {
console.error("Failed to fetch patient data");
return;
}
console.log("Patient Data:", patientData);
await this.fetchCurrentUser();
try {
// 1️⃣ Fetch current user first
await this.fetchCurrentUser();
// 2️⃣ Then fetch their services
if (this.state.currentUser?.id) {
await this.fetchServices(this.state.currentUser.id);
} else {
console.error("❌ No user ID found in currentUser");
}
await this.initializeCornerstone();
// Use study_id from patient data instead of hardcoded value
const studyid = patientData.study_id || "3c475322-dd16d05c-ceaf7688-4d64be59-57de70f0";
// Setting cache size
cornerstone.cache.setMaxCacheSize(3 * 1024 * 1024 * 1024); // 3 GB safe for most systems
cornerstone.setUseSharedArrayBuffer(false);
const elements = [
document.getElementById('viewport1'),
];
// Create rendering engine if it doesn't exist
if (!this.renderingEngine) {
this.renderingEngine = new cornerstone.RenderingEngine(renderingEngineId);
}
// Check if tool group already exists
this.toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
if (!this.toolGroup) {
// Create tool groups for storing all tools
this.toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId);
for (const [key, value] of Object.entries(Tools)){
cornerstoneTools.addTool(value);
this.toolGroup.addTool(value.toolName);
}
// Enable tools
this.toolGroup.setToolConfiguration(cornerstoneTools.StackScrollTool.toolName, {
bindings: [
{
mouseButton: cornerstoneTools.Enums.MouseBindings.Primary,
},
],
});
// Set scroll active
this.toolGroup.setToolActive(cornerstoneTools.StackScrollMouseWheelTool.toolName);
this.toolGroup.setToolConfiguration(cornerstoneTools.CrosshairsTool.toolName, {
bindings: [
{
mouseButton: cornerstoneTools.Enums.MouseBindings.Primary,
},
],
});
this.toolGroup.setToolConfiguration(cornerstoneTools.PlanarFreehandROITool.toolName, {
calculateStats: true
});
this.toolGroup.setToolConfiguration(cornerstoneTools.HeightTool.toolName, {
calculateStats: true
});
}
// Define 4 stack viewports with viewport id, viewport type, DOM element to be used
const first_viewport = {
viewportId: viewportIds[0],
type: cornerstone.Enums.ViewportType.STACK,
element: elements[0],
};
this.viewport_list = {
first: first_viewport,
};
// Enable first_viewport, make it the previously selected viewport, set its properties, add the toolgroup
this.renderingEngine.enableElement(first_viewport);
this.selected_viewport = viewportIds[0];
this.prev_selected_element = elements[0];
const viewport = this.renderingEngine.getViewport(this.selected_viewport);
viewport.setProperties({rotation: 0});
// Add viewport to toolgroup if not already added
this.toolGroup.addViewport(viewportIds[0], renderingEngineId);
// Function to cache images and metadata, create volumes if needed
if (!this.cornerstoneProcessed) {
this.cornerstoneProcessed = true;
this.cornerstone(studyid);
}
// Event listeners for viewports
elements.forEach((item, i) => {
// Initial event listener for stack viewport to capture image index
const stackNewImageListener = function() {
let currViewport = this.renderingEngine.getViewport(viewportIds[i]);
let index = currViewport.getCurrentImageIdIndex() + 1;
// Update image index
let indexElem = document.getElementById(indexMap[currViewport.id]);
if (indexElem) {
indexElem.innerHTML = index;
}
}.bind(this);
item.addEventListener(cornerstone.EVENTS.STACK_NEW_IMAGE, stackNewImageListener);
this.eventListeners.push({element: item, type: cornerstone.EVENTS.STACK_NEW_IMAGE, listener: stackNewImageListener});
const clickListener = function() {
// Get clicked viewport ID
const clickedViewportId = viewportIds[i];
if (this.selected_viewport !== clickedViewportId) {
// Update selected viewport
this.selected_viewport = clickedViewportId;
// Reset border style
if (this.prev_selected_element) {
this.prev_selected_element.style.borderColor = 'white';
}
item.style.borderColor = 'red';
this.prev_selected_element = item;
// Dynamically update reference lines config
const targetViewports = viewportIds.filter(id => id !== this.selected_viewport);
this.toolGroup.setToolConfiguration(cornerstoneTools.ReferenceLinesTool.toolName, {
sourceViewportId: this.selected_viewport,
targetViewportIds: targetViewports,
});
// Force render update
this.renderingEngine.render();
}
}.bind(this);
item.addEventListener('click', clickListener);
this.eventListeners.push({element: item, type: 'click', listener: clickListener});
});
} catch (error) {
console.error("Error in componentDidMount:", error);
this.setState({
error: error.message,
loading: false
});
}
}
render() {
const { showDetails, status, showReportEditor, studyData, reportFrmData, splitterPosition, isResizing, loading, error, patientData } = this.state;
<div className="viewport-div">
<div className="viewport" id='viewport1' data-value='first' onDragOver={e => this.allowDrop(e)} onDrop={e => this.drop(e)}></div>
</div>
</div>
</div>
</div>
);
}
}
export default ClinetViwer;