start adding volume toolgroup
This commit is contained in:
+170
-175
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@cornerstonejs/core"
|
} from "@cornerstonejs/core"
|
||||||
import { init as csRenderInit } from "@cornerstonejs/core"
|
import { init as csRenderInit } from "@cornerstonejs/core"
|
||||||
import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool } from "@cornerstonejs/tools"
|
import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool } from "@cornerstonejs/tools"
|
||||||
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool } from "@cornerstonejs/tools"
|
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool } from "@cornerstonejs/tools"
|
||||||
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
|
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
|
||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes';
|
import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes';
|
||||||
@@ -22,6 +22,8 @@ const { MouseBindings, KeyboardBindings } = csToolsEnums;
|
|||||||
|
|
||||||
const { IMAGE_RENDERED } = Enums.Events;
|
const { IMAGE_RENDERED } = Enums.Events;
|
||||||
|
|
||||||
|
const STACK_TOOL_GROUP_ID = 'STACK_TOOL_GROUP_ID';
|
||||||
|
const VOLUME_TOOL_GROUP_ID = 'VOLUME_TOOL_GROUP_ID';
|
||||||
const toolGroupId = 'STACK_TOOL_GROUP_ID';
|
const toolGroupId = 'STACK_TOOL_GROUP_ID';
|
||||||
|
|
||||||
// Common CT presets (add more as needed)
|
// Common CT presets (add more as needed)
|
||||||
@@ -84,6 +86,8 @@ function App() {
|
|||||||
// Store imageIds globally after loading so they can be reused
|
// Store imageIds globally after loading so they can be reused
|
||||||
const [loadedImageIds, setLoadedImageIds] = useState<string[] | null>(null);
|
const [loadedImageIds, setLoadedImageIds] = useState<string[] | null>(null);
|
||||||
|
|
||||||
|
const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true);
|
||||||
|
|
||||||
// Wrap setLoadedImageIds to also cache DICOM metadata
|
// Wrap setLoadedImageIds to also cache DICOM metadata
|
||||||
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
||||||
for (const imageId of imageIds) {
|
for (const imageId of imageIds) {
|
||||||
@@ -392,6 +396,7 @@ function App() {
|
|||||||
// 4. Setup tool group
|
// 4. Setup tool group
|
||||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || setupTools();
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || setupTools();
|
||||||
|
|
||||||
|
|
||||||
// 6. Enable and configure each viewport
|
// 6. Enable and configure each viewport
|
||||||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||||||
const viewportId = `CT_${i}`;
|
const viewportId = `CT_${i}`;
|
||||||
@@ -420,65 +425,48 @@ function App() {
|
|||||||
// Add tool group only once
|
// Add tool group only once
|
||||||
toolGroup.addViewport(viewportId, renderingEngineId);
|
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
|
||||||
|
// Inside your setup loop for each viewport
|
||||||
|
if (viewportModes[i] === 'volume' && crossReferenceEnabled) {
|
||||||
|
if (!toolGroup.hasTool(CrosshairsTool.toolName)) {
|
||||||
|
toolGroup.addTool(CrosshairsTool.toolName);
|
||||||
|
}
|
||||||
|
toolGroup.setToolEnabled(CrosshairsTool.toolName);
|
||||||
|
toolGroup.setToolActive(CrosshairsTool.toolName, {
|
||||||
|
bindings: [{ mouseButton: MouseBindings.Primary }],
|
||||||
|
});
|
||||||
|
} else if (toolGroup.hasTool(CrosshairsTool.toolName)) {
|
||||||
|
toolGroup.setToolDisabled(CrosshairsTool.toolName);
|
||||||
|
}
|
||||||
|
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
if (!viewport) continue;
|
if (!viewport) continue;
|
||||||
|
|
||||||
|
if (!element._resizeObserver) {
|
||||||
|
const resizeObserver = new window.ResizeObserver(() => {
|
||||||
|
console.log("Resize observer triggered for viewport", i);
|
||||||
|
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
|
||||||
|
renderingEngine.resize();
|
||||||
|
// Always get the latest viewport instance
|
||||||
|
const vp = renderingEngine.getViewport(viewportId);
|
||||||
|
if (vp && typeof vp.resize === "function") {
|
||||||
|
vp.resize();
|
||||||
|
}
|
||||||
|
if (vp && typeof vp.render === "function") {
|
||||||
|
vp.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
element._resizeObserver = resizeObserver;
|
||||||
|
}
|
||||||
|
|
||||||
if (viewportModes[i] === 'stack') {
|
if (viewportModes[i] === 'stack') {
|
||||||
if (imageIds) {
|
if (imageIds) {
|
||||||
viewport.setStack(imageIds);
|
viewport.setStack(imageIds);
|
||||||
}
|
}
|
||||||
// Restore scroll, properties, etc.
|
// Restore scroll, properties, etc.
|
||||||
} else if (viewportModes[i] === 'volume') {
|
} else if (viewportModes[i] === 'volume') {
|
||||||
//// Ensure all images are loaded before proceeding
|
|
||||||
//const notLoaded = imageIds.filter(id => !metaData.get('imagePlaneModule', id));
|
|
||||||
//if (notLoaded.length > 0) {
|
|
||||||
// // Force metadata loading for missing images
|
|
||||||
// console.log("Forcing metadata loading for missing images:", notLoaded);
|
|
||||||
// await Promise.all(
|
|
||||||
// notLoaded.map(async id => {
|
|
||||||
// try {
|
|
||||||
// // Remove "wadouri:" prefix to get the URL
|
|
||||||
// const url = id.startsWith("wadouri:") ? id.slice(8) : id;
|
|
||||||
// if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
|
||||||
// cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url)
|
|
||||||
// } else {
|
|
||||||
// console.warn("MetaDataManager not found. Unable to cache metadata.");
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// console.warn(`Failed to force-load metadata for ${id}:`, e);
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
//}
|
|
||||||
//// After forcing, check again
|
|
||||||
//const stillNotLoaded = imageIds.filter(id => !metaData.get('imagePlaneModule', id));
|
|
||||||
//if (stillNotLoaded.length > 0) {
|
|
||||||
// alert("Some images could not be loaded for volume rendering.");
|
|
||||||
// setViewportModes(prev => {
|
|
||||||
// const next = [...prev];
|
|
||||||
// next[i] = 'stack';
|
|
||||||
// return next;
|
|
||||||
// });
|
|
||||||
// continue;
|
|
||||||
//}
|
|
||||||
//// Check metadata for all imageIds
|
|
||||||
//const missingMeta = imageIds.filter(id => {
|
|
||||||
// const meta = metaData.get('imagePlaneModule', id);
|
|
||||||
// return !(meta && meta.imagePositionPatient && meta.imageOrientationPatient && meta.pixelSpacing);
|
|
||||||
//});
|
|
||||||
//if (missingMeta.length > 0) {
|
|
||||||
// console.warn(
|
|
||||||
// "The following imageIds are missing required DICOM metadata for volume rendering:",
|
|
||||||
// missingMeta
|
|
||||||
// );
|
|
||||||
// setViewportModes(prev => {
|
|
||||||
// const next = [...prev];
|
|
||||||
// next[i] = 'stack';
|
|
||||||
// return next;
|
|
||||||
// });
|
|
||||||
// alert("Some images are missing required DICOM metadata for volume rendering.");
|
|
||||||
// continue;
|
|
||||||
//}
|
|
||||||
// Example: create and set a volume (you may need to adapt this for your data)
|
// Example: create and set a volume (you may need to adapt this for your data)
|
||||||
const volumeId = 'myVolumeId';
|
const volumeId = 'myVolumeId';
|
||||||
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds });
|
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds });
|
||||||
@@ -531,18 +519,7 @@ function App() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach ResizeObserver to handle dynamic resizing (only once)
|
|
||||||
if (!element._resizeObserver) {
|
|
||||||
const resizeObserver = new window.ResizeObserver(() => {
|
|
||||||
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
|
|
||||||
renderingEngine.resize();
|
|
||||||
viewport.resize();
|
|
||||||
viewport.render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resizeObserver.observe(element);
|
|
||||||
element._resizeObserver = resizeObserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always (re-)attach overlay update for each viewport
|
// Always (re-)attach overlay update for each viewport
|
||||||
if (element._overlayListener) {
|
if (element._overlayListener) {
|
||||||
@@ -884,12 +861,21 @@ function App() {
|
|||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
{viewportModes[i] === "stack" ? (
|
||||||
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
<>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
||||||
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Volume mode: do not show image position
|
||||||
|
<div>
|
||||||
|
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preset menu (bottom right of viewport) */}
|
{/* Preset menu (bottom right of viewport) */}
|
||||||
@@ -978,6 +964,102 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Position bar inside each viewport */}
|
||||||
|
{viewportModes[i] === "stack" && loadedImageIds && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "10%",
|
||||||
|
right: 8,
|
||||||
|
width: "12px",
|
||||||
|
height: "80%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
minHeight: 0,
|
||||||
|
zIndex: 30,
|
||||||
|
display: loadedImageIds.length > 0 ? "flex" : "none",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "center",
|
||||||
|
pointerEvents: "none",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "8px",
|
||||||
|
height: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
minHeight: 0,
|
||||||
|
borderRadius: "6px",
|
||||||
|
background: "#222",
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
boxShadow: "0 0 8px #2196f3cc",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loadedImageIds.map((id, idx) => {
|
||||||
|
const loaded = !!metaData.get("imagePlaneModule", id);
|
||||||
|
const renderingEngine = renderingEngineRef.current;
|
||||||
|
let currentIdx = -1;
|
||||||
|
if (
|
||||||
|
renderingEngine &&
|
||||||
|
typeof renderingEngine.getViewport === "function"
|
||||||
|
) {
|
||||||
|
const viewport = renderingEngine.getViewport(`CT_${i}`);
|
||||||
|
if (viewport && typeof viewport.getCurrentImageIdIndex === "function") {
|
||||||
|
currentIdx = viewport.getCurrentImageIdIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isCurrent = idx === currentIdx;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: `${100 / loadedImageIds.length}%`,
|
||||||
|
background: isCurrent
|
||||||
|
? "linear-gradient(to right, #ffeb3b 60%, #ff9800 100%)"
|
||||||
|
: loaded
|
||||||
|
? "linear-gradient(to right, #4caf50 60%, #2196f3 100%)"
|
||||||
|
: "#444",
|
||||||
|
opacity: loaded ? 0.95 : 0.4,
|
||||||
|
transition: "background 0.3s, opacity 0.3s",
|
||||||
|
borderBottom:
|
||||||
|
idx < loadedImageIds.length - 1
|
||||||
|
? "1px solid #222"
|
||||||
|
: "none",
|
||||||
|
cursor: loaded ? "pointer" : "default",
|
||||||
|
pointerEvents: loaded ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
isCurrent
|
||||||
|
? `Current image #${idx + 1}`
|
||||||
|
: loaded
|
||||||
|
? `Go to image #${idx + 1}`
|
||||||
|
: `Loading...`
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
const renderingEngine = renderingEngineRef.current;
|
||||||
|
const viewport = renderingEngine?.getViewport(`CT_${i}`);
|
||||||
|
if (
|
||||||
|
viewport &&
|
||||||
|
typeof viewport.setImageIdIndex === "function" &&
|
||||||
|
viewportModes[i] === "stack"
|
||||||
|
) {
|
||||||
|
viewport.setImageIdIndex(idx);
|
||||||
|
viewport.render();
|
||||||
|
updateOverlay(i, viewport);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1116,6 +1198,23 @@ function App() {
|
|||||||
>
|
>
|
||||||
Show DICOM Metadata
|
Show DICOM Metadata
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: "10px 18px",
|
||||||
|
background: crossReferenceEnabled ? "#1976d2" : "#333",
|
||||||
|
color: "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "15px",
|
||||||
|
marginBottom: "18px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
onClick={() => setCrossReferenceEnabled((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{crossReferenceEnabled ? "Disable" : "Enable"} Cross Reference Lines
|
||||||
|
</button>
|
||||||
{/* Add more menu items here if needed */}
|
{/* Add more menu items here if needed */}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1215,112 +1314,7 @@ function App() {
|
|||||||
>
|
>
|
||||||
{viewports}
|
{viewports}
|
||||||
|
|
||||||
{/* Per-viewport status bars, positioned inside each viewport */}
|
|
||||||
{Array.from({ length: viewportGrid.rows * viewportGrid.cols }).map((_, vpIdx) => {
|
|
||||||
// Only show for visible viewports
|
|
||||||
if (vpIdx >= loadedImageIds?.length) return null;
|
|
||||||
// Get imageIds for this viewport (all viewports share the same stack, adapt if needed)
|
|
||||||
const imageIds = loadedImageIds || [];
|
|
||||||
// Get current image index for this viewport
|
|
||||||
let currentIdx = -1;
|
|
||||||
const renderingEngine = renderingEngineRef.current;
|
|
||||||
if (
|
|
||||||
renderingEngine &&
|
|
||||||
typeof renderingEngine.getViewport === "function" &&
|
|
||||||
viewportModes[vpIdx] === "stack"
|
|
||||||
) {
|
|
||||||
const viewport = renderingEngine.getViewport(`CT_${vpIdx}`);
|
|
||||||
if (viewport && typeof viewport.getCurrentImageIdIndex === "function") {
|
|
||||||
currentIdx = viewport.getCurrentImageIdIndex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Position the bar absolutely inside the viewport grid cell
|
|
||||||
// Calculate row and column for this viewport
|
|
||||||
const row = Math.floor(vpIdx / viewportGrid.cols) + 1;
|
|
||||||
const col = (vpIdx % viewportGrid.cols) + 1;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`statusbar-${vpIdx}`}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
// These CSS grid calculations align the bar with the viewport cell
|
|
||||||
top: `calc((100% / ${viewportGrid.rows}) * (${row - 1}) + 10%)`,
|
|
||||||
left: `calc((100% / ${viewportGrid.cols}) * ${col} - 24px)`,
|
|
||||||
width: "16px",
|
|
||||||
height: "80vh",
|
|
||||||
zIndex: 50,
|
|
||||||
display: imageIds.length > 0 && viewportModes[vpIdx] === "stack" ? "flex" : "none",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "center",
|
|
||||||
pointerEvents: "none",
|
|
||||||
flexDirection: "column",
|
|
||||||
padding: "8px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "8px",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: "6px",
|
|
||||||
background: "#222",
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
boxShadow: "0 0 8px #2196f3cc",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{imageIds.map((id, idx) => {
|
|
||||||
const loaded = !!metaData.get("imagePlaneModule", id);
|
|
||||||
const isCurrent = idx === currentIdx;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: `${100 / imageIds.length}%`,
|
|
||||||
background: isCurrent
|
|
||||||
? "linear-gradient(to right, #ffeb3b 60%, #ff9800 100%)"
|
|
||||||
: loaded
|
|
||||||
? "linear-gradient(to right, #4caf50 60%, #2196f3 100%)"
|
|
||||||
: "#444",
|
|
||||||
opacity: loaded ? 0.95 : 0.4,
|
|
||||||
transition: "background 0.3s, opacity 0.3s",
|
|
||||||
borderBottom:
|
|
||||||
idx < imageIds.length - 1
|
|
||||||
? "1px solid #222"
|
|
||||||
: "none",
|
|
||||||
cursor: loaded ? "pointer" : "default",
|
|
||||||
pointerEvents: loaded ? "auto" : "none",
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
isCurrent
|
|
||||||
? `Current image #${idx + 1}`
|
|
||||||
: loaded
|
|
||||||
? `Go to image #${idx + 1}`
|
|
||||||
: `Loading...`
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
const renderingEngine = renderingEngineRef.current;
|
|
||||||
const viewport = renderingEngine?.getViewport(`CT_${vpIdx}`);
|
|
||||||
if (
|
|
||||||
viewport &&
|
|
||||||
typeof viewport.setImageIdIndex === "function" &&
|
|
||||||
viewportModes[vpIdx] === "stack"
|
|
||||||
) {
|
|
||||||
viewport.setImageIdIndex(idx);
|
|
||||||
viewport.render();
|
|
||||||
updateOverlay(vpIdx, viewport);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -1520,6 +1514,7 @@ function setupTools() {
|
|||||||
cornerstoneTools.addTool(PlanarFreehandROITool)
|
cornerstoneTools.addTool(PlanarFreehandROITool)
|
||||||
cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool)
|
cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool)
|
||||||
cornerstoneTools.addTool(SculptorTool)
|
cornerstoneTools.addTool(SculptorTool)
|
||||||
|
cornerstoneTools.addTool(CrosshairsTool)
|
||||||
|
|
||||||
// Create tool group ONCE
|
// Create tool group ONCE
|
||||||
let toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
let toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||||
|
|||||||
Reference in New Issue
Block a user