1964 lines
67 KiB
TypeScript
1964 lines
67 KiB
TypeScript
import { useEffect, useRef, useCallback, useState } from "react"
|
||
import { createImageIds, availableImageIds } from "./lib/createImageIds.ts"
|
||
import {
|
||
RenderingEngine,
|
||
Enums,
|
||
} from "@cornerstonejs/core"
|
||
import { init as csRenderInit, utilities as csUtilities } from "@cornerstonejs/core"
|
||
import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool, utilities as csToolsUtilities } from "@cornerstonejs/tools"
|
||
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool, ReferenceCursors } from "@cornerstonejs/tools"
|
||
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
|
||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||
import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes';
|
||
import { segmentation, Enums as csEnums } from '@cornerstonejs/tools';
|
||
import { voi } from "@cornerstonejs/tools/utilities"
|
||
import { readDicomElementExplicit } from "dicom-parser"
|
||
import { volumeLoader } from '@cornerstonejs/core';
|
||
import { metaData } from '@cornerstonejs/core';
|
||
import * as dicomParser from "dicom-parser";
|
||
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
|
||
|
||
|
||
window.cornerstoneTools = cornerstoneTools;
|
||
|
||
const { MouseBindings, KeyboardBindings } = csToolsEnums;
|
||
|
||
const { IMAGE_RENDERED } = Enums.Events;
|
||
|
||
const STACK_TOOL_GROUP_ID = 'STACK_TOOL_GROUP_ID';
|
||
const VOLUME_TOOL_GROUP_ID = 'VOLUME_TOOL_GROUP_ID';
|
||
const MAIN_TOOL_GROUP_ID = 'MAIN_TOOL_GROUP_ID';
|
||
|
||
// Common CT presets (add more as needed)
|
||
const WINDOW_LEVEL_PRESETS = [
|
||
{ name: "Soft Tissue", center: 40, width: 400 },
|
||
{ name: "Lung", center: -600, width: 1500 },
|
||
{ name: "Bone", center: 300, width: 1500 },
|
||
{ name: "Brain", center: 40, width: 80 },
|
||
{ name: "Abdomen", center: 60, width: 400 },
|
||
]
|
||
|
||
// Tool options for dropdowns
|
||
const TOOL_OPTIONS = [
|
||
{ label: "Window/Level", value: WindowLevelTool.toolName },
|
||
{ label: "Pan", value: PanTool.toolName },
|
||
{ label: "Zoom", value: ZoomTool.toolName },
|
||
{ label: "Stack Scroll", value: StackScrollTool.toolName },
|
||
{ label: "Rotate", value: PlanarRotateTool.toolName },
|
||
{ label: "Length", value: LengthTool.toolName },
|
||
{ label: "Probe", value: ProbeTool.toolName },
|
||
{ label: "Arrow Annotate", value: ArrowAnnotateTool.toolName },
|
||
{ label: "Rectangle ROI", value: RectangleROITool.toolName },
|
||
{ label: "Ellipse ROI", value: EllipticalROITool.toolName },
|
||
{ label: "Freehand ROI", value: PlanarFreehandROITool.toolName },
|
||
{ label: "Freehand Contour Segmentation", value: PlanarFreehandContourSegmentationTool.toolName },
|
||
{ label: "Sculptor", value: SculptorTool.toolName },
|
||
{ label: "Reference Cursors", value: ReferenceCursors.toolName },
|
||
]
|
||
|
||
// Mouse button labels
|
||
const MOUSE_BUTTONS = [
|
||
{ label: "Left", value: "Primary" },
|
||
{ label: "Middle", value: "Auxiliary" },
|
||
{ label: "Right", value: "Secondary" },
|
||
]
|
||
const ALL_MOUSE_BINDINGS = [
|
||
{ label: "Left", value: "Primary", mask: 1 },
|
||
{ label: "Middle", value: "Auxiliary", mask: 4 },
|
||
{ label: "Right", value: "Secondary", mask: 2 },
|
||
{ label: "Left + Right", value: "Primary_and_Secondary", mask: 3 },
|
||
{ label: "Left + Middle", value: "Primary_and_Auxiliary", mask: 5 },
|
||
{ label: "Right + Middle", value: "Secondary_and_Auxiliary", mask: 6 },
|
||
{ label: "Left + Right + Middle", value: "Primary_and_Secondary_and_Auxiliary", mask: 7 },
|
||
{ label: "Fourth Button", value: "Fourth_Button", mask: 8 },
|
||
{ label: "Fifth Button", value: "Fifth_Button", mask: 16 },
|
||
{ label: "Mouse Wheel", value: "Wheel", mask: 524288 },
|
||
{ label: "Mouse Wheel + Left", value: "Wheel_Primary", mask: 524289 },
|
||
];
|
||
|
||
|
||
// App definintion
|
||
function App() {
|
||
const elementRef = useRef<HTMLDivElement>(null)
|
||
const viewportRef = useRef<any>(null) // Store the viewport instance
|
||
const running = useRef(false)
|
||
|
||
// State to control preset menu open/close
|
||
const [presetsOpen, setPresetsOpen] = useState(false);
|
||
|
||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||
|
||
// State for overlay info
|
||
const [imageInfo, setImageInfo] = useState({
|
||
index: 0,
|
||
total: 0,
|
||
windowCenter: "",
|
||
windowWidth: "",
|
||
})
|
||
|
||
const MAX_VIEWPORTS = 9;
|
||
const renderingEngineId = "mainRenderingEngine";
|
||
const renderingEngineRef = useRef<any>(null);
|
||
|
||
const [availableStacks, setAvailableStacks] = useState<string[][]>([]);
|
||
const [activeViewport, setActiveViewport] = useState<number | null>(0);
|
||
const [viewportImageIds, setViewportImageIds] = useState<string[][]>(
|
||
() => Array(MAX_VIEWPORTS).fill([])
|
||
);
|
||
const [selectedStackIdx, setSelectedStackIdx] = useState<number[]>(
|
||
() => Array(MAX_VIEWPORTS).fill(0)
|
||
);
|
||
|
||
// Load available stacks on mount
|
||
useEffect(() => {
|
||
availableImageIds().then(stacks => {
|
||
setAvailableStacks(stacks);
|
||
// Optionally initialize each viewport with the first stack
|
||
setLoadedImageIdsAndCacheMeta(stacks[0]);
|
||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
|
||
});
|
||
}, []);
|
||
|
||
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||
const stack = availableStacks[stackIdx];
|
||
|
||
if (!stack) return;
|
||
setViewportImageIds(prev => {
|
||
const next = [...prev];
|
||
next[viewportIdx] = stack;
|
||
return next;
|
||
});
|
||
setSelectedStackIdx(prev => {
|
||
const next = [...prev];
|
||
next[viewportIdx] = stackIdx;
|
||
return next;
|
||
});
|
||
|
||
// Cache imageIds and load metadata for this stack
|
||
await setLoadedImageIdsAndCacheMeta(stack);
|
||
};
|
||
|
||
const [crossHairsEnabled, setCrossHairsEnabled] = useState(false);
|
||
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(true);
|
||
|
||
const [showAdvancedMouseBindings, setShowAdvancedMouseBindings] = useState(false);
|
||
|
||
const sortViewportImageIdsBySliceLocation = () => {
|
||
setViewportImageIds(prev => {
|
||
const next = prev.map((imageIds, idx) => {
|
||
if (!imageIds || imageIds.length === 0) return imageIds;
|
||
|
||
const withSortKey = imageIds.map(id => {
|
||
const plane = metaData.get('imagePlaneModule', id);
|
||
let sortKey = null;
|
||
if (plane) {
|
||
if (plane.sliceLocation !== undefined) {
|
||
sortKey = Number(plane.sliceLocation);
|
||
} else if (
|
||
plane.imagePositionPatient &&
|
||
Array.isArray(plane.imagePositionPatient) &&
|
||
plane.imagePositionPatient.length === 3
|
||
) {
|
||
sortKey = Number(plane.imagePositionPatient[2]);
|
||
}
|
||
}
|
||
return { imageId: id, sortKey };
|
||
});
|
||
|
||
const sorted = [...withSortKey].sort((a, b) => {
|
||
if (a.sortKey !== null && b.sortKey !== null) {
|
||
return a.sortKey - b.sortKey;
|
||
}
|
||
return a.imageId.localeCompare(b.imageId);
|
||
});
|
||
|
||
const sortedIds = sorted.map(obj => obj.imageId);
|
||
|
||
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
|
||
return imageIds;
|
||
}
|
||
return sortedIds;
|
||
});
|
||
return next;
|
||
});
|
||
};
|
||
|
||
// Wrap setLoadedImageIds to also cache DICOM metadata
|
||
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
||
const loadPromises: Promise<any>[] = [];
|
||
|
||
for (const imageId of imageIds) {
|
||
if (imageId.startsWith("wadouri:")) {
|
||
try {
|
||
const url = imageId.slice(8);
|
||
if (url.startsWith("blob:") || url.startsWith("/")) {
|
||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||
// If load returns a promise, collect it
|
||
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
||
if (result && typeof result.then === "function") {
|
||
loadPromises.push(result);
|
||
}
|
||
} else {
|
||
console.warn("MetaDataManager not found. Unable to cache metadata.");
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn(`Failed to cache metadata for ${imageId}:`, e);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Wait for all metadata loads to finish
|
||
await Promise.all(loadPromises);
|
||
|
||
// Trigger sort if enabled
|
||
if (orderBySliceLocation) {
|
||
sortViewportImageIdsBySliceLocation();
|
||
}
|
||
};
|
||
|
||
|
||
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
||
const [viewportMenuOpen, setViewportMenuOpen] = useState(false)
|
||
|
||
// Store refs for each viewport
|
||
const viewportRefs = useRef<Array<HTMLDivElement | null>>([])
|
||
|
||
// Store the properties and scroll position of existing viewports before grid change
|
||
const prevViewportProps = useRef<Record<number, any>>({});
|
||
const prevScrollIndices = useRef<Record<number, number>>({});
|
||
|
||
const [orderBySliceLocation, setOrderBySliceLocation] = useState(true);
|
||
|
||
const [altEnablesReferenceCursors, setAltEnablesReferenceCursors] = useState(true);
|
||
|
||
// Helper to generate grid options
|
||
const gridOptions = []
|
||
for (let rows = 1; rows <= 3; rows++) {
|
||
for (let cols = 1; cols <= 3; cols++) {
|
||
gridOptions.push({ rows, cols })
|
||
}
|
||
}
|
||
|
||
// State for overlay info per viewport
|
||
const [imageInfos, setImageInfos] = useState<Array<{
|
||
index: number,
|
||
total: number,
|
||
windowCenter: string,
|
||
windowWidth: string,
|
||
}>>(() =>
|
||
Array(viewportGrid.rows * viewportGrid.cols).fill({
|
||
index: 0,
|
||
total: 0,
|
||
windowCenter: "",
|
||
windowWidth: "",
|
||
})
|
||
);
|
||
|
||
const [viewportModes, setViewportModes] = useState<Array<'stack' | 'volume'>>(
|
||
() => Array(MAX_VIEWPORTS).fill('stack')
|
||
);
|
||
|
||
// State for preset menu open/close per viewport
|
||
const [presetsOpenArr, setPresetsOpenArr] = useState<boolean[]>(
|
||
() => Array(viewportGrid.rows * viewportGrid.cols).fill(false)
|
||
);
|
||
|
||
// State for selected tool per mouse button
|
||
const [mouseToolBindings, setMouseToolBindings] = useState({
|
||
Primary: StackScrollTool.toolName,
|
||
Auxiliary: PanTool.toolName,
|
||
Secondary: WindowLevelTool.toolName,
|
||
Primary_and_Secondary: ZoomTool.toolName, // 1 (left) + 2 (right) = 3
|
||
Fourth_Button: PlanarRotateTool.toolName, // 16
|
||
Wheel: StackScrollTool.toolName, // 524288
|
||
|
||
})
|
||
const [ctrlMouseToolBindings, setCtrlMouseToolBindings] = useState({
|
||
Primary: LengthTool.toolName,
|
||
Auxiliary: ProbeTool.toolName,
|
||
Secondary: ArrowAnnotateTool.toolName,
|
||
})
|
||
|
||
// Track if Ctrl is pressed
|
||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
if (e.ctrlKey) setCtrlPressed(true)
|
||
}
|
||
const handleKeyUp = (e: KeyboardEvent) => {
|
||
if (!e.ctrlKey) setCtrlPressed(false)
|
||
}
|
||
window.addEventListener("keydown", handleKeyDown)
|
||
window.addEventListener("keyup", handleKeyUp)
|
||
return () => {
|
||
window.removeEventListener("keydown", handleKeyDown)
|
||
window.removeEventListener("keyup", handleKeyUp)
|
||
}
|
||
}, [])
|
||
|
||
const [metaModalOpen, setMetaModalOpen] = useState(false);
|
||
const [metaModalContent, setMetaModalContent] = useState<any>(null);
|
||
|
||
// Add this function to gather and format metadata:
|
||
const handleShowMetadata = () => {
|
||
// Find the first visible viewport with images
|
||
const firstIdx = viewportImageIds.findIndex(ids => ids && ids.length > 0);
|
||
if (firstIdx === -1) {
|
||
setMetaModalContent("No images loaded.");
|
||
setMetaModalOpen(true);
|
||
return;
|
||
}
|
||
const imageId = viewportImageIds[firstIdx][0];
|
||
|
||
// Gather all available metadata modules for this imageId
|
||
const modules = [
|
||
"imagePlaneModule",
|
||
"generalSeriesModule",
|
||
"generalStudyModule",
|
||
"patientModule",
|
||
"imagePixelModule",
|
||
"voiLutModule",
|
||
"modalityLutModule",
|
||
"sopCommonModule",
|
||
"petIsotopeModule",
|
||
"multiframeModule",
|
||
"cineModule",
|
||
"overlayPlaneModule",
|
||
"generalImageModule",
|
||
"stackModule",
|
||
"voiLutModule",
|
||
"modalityLutModule",
|
||
"petSeriesModule",
|
||
"petImageModule",
|
||
"petIsotopeModule",
|
||
"petSeriesModule",
|
||
"petImageModule",
|
||
"petIsotopeModule",
|
||
"petSeriesModule",
|
||
"petImageModule",
|
||
"petIsotopeModule",
|
||
"petSeriesModule",
|
||
"petImageModule",
|
||
"petIsotopeModule",
|
||
// Add more if your app or cornerstone supports them
|
||
];
|
||
|
||
const allMeta: Record<string, any> = {};
|
||
modules.forEach((mod) => {
|
||
const val = metaData.get(mod, imageId);
|
||
if (val !== undefined) allMeta[mod] = val;
|
||
});
|
||
|
||
setMetaModalContent(
|
||
<pre style={{ maxHeight: 600, overflow: "auto", fontSize: 13, color: "#fff" }}>
|
||
{JSON.stringify(allMeta, null, 2)}
|
||
</pre>
|
||
);
|
||
setMetaModalOpen(true);
|
||
};
|
||
|
||
const [altPressed, setAltPressed] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
if (e.altKey) setAltPressed(true);
|
||
};
|
||
const handleKeyUp = (e: KeyboardEvent) => {
|
||
if (!e.altKey) setAltPressed(false);
|
||
};
|
||
window.addEventListener("keydown", handleKeyDown);
|
||
window.addEventListener("keyup", handleKeyUp);
|
||
return () => {
|
||
window.removeEventListener("keydown", handleKeyDown);
|
||
window.removeEventListener("keyup", handleKeyUp);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!altEnablesReferenceCursors) return;
|
||
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||
if (!mainToolGroup) return;
|
||
if (altPressed) {
|
||
if (!mainToolGroup.hasTool(ReferenceCursors.toolName)) {
|
||
mainToolGroup.addTool(ReferenceCursors.toolName);
|
||
}
|
||
mainToolGroup.setToolEnabled(ReferenceCursors.toolName);
|
||
mainToolGroup.setToolActive(ReferenceCursors.toolName);
|
||
} else {
|
||
if (mainToolGroup.hasTool(ReferenceCursors.toolName)) {
|
||
mainToolGroup.setToolDisabled(ReferenceCursors.toolName);
|
||
}
|
||
}
|
||
}, [altPressed, altEnablesReferenceCursors]);
|
||
|
||
// Helper to apply all mouse tool bindings (normal or ctrl)
|
||
const applyMouseToolBindings = useCallback(() => {
|
||
// Apply to both stack and volume tool groups
|
||
const mainToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||
|
||
[mainToolGroup].forEach(toolGroup => {
|
||
if (toolGroup) {
|
||
// Set all tools to passive first, removing all bindings
|
||
TOOL_OPTIONS.forEach(opt => {
|
||
toolGroup.setToolPassive(opt.value, { removeAllBindings: true });
|
||
});
|
||
|
||
toolGroup.setToolDisabled(ReferenceCursors.toolName);
|
||
|
||
// Collect all bindings for each tool
|
||
const toolBindings: Record<string, Array<any>> = {};
|
||
|
||
// Normal bindings
|
||
Object.entries(mouseToolBindings).forEach(([btn, tool]) => {
|
||
if (!toolBindings[tool]) toolBindings[tool] = [];
|
||
// Find the mask for this binding
|
||
const binding = ALL_MOUSE_BINDINGS.find(b => b.value === btn);
|
||
if (binding) {
|
||
toolBindings[tool].push({ mouseButton: binding.mask });
|
||
} else if (MouseBindings[btn]) {
|
||
// Fallback for legacy bindings
|
||
toolBindings[tool].push({ mouseButton: MouseBindings[btn] });
|
||
}
|
||
});
|
||
// Ctrl bindings (now supports advanced mouse bindings)
|
||
Object.entries(ctrlMouseToolBindings).forEach(([btn, tool]) => {
|
||
if (!tool) return;
|
||
if (!toolBindings[tool]) toolBindings[tool] = [];
|
||
const binding = ALL_MOUSE_BINDINGS.find(b => b.value === btn);
|
||
if (binding) {
|
||
toolBindings[tool].push({
|
||
mouseButton: binding.mask,
|
||
modifierKey: KeyboardBindings.Ctrl,
|
||
});
|
||
} else if (MouseBindings[btn]) {
|
||
toolBindings[tool].push({
|
||
mouseButton: MouseBindings[btn],
|
||
modifierKey: KeyboardBindings.Ctrl,
|
||
});
|
||
}
|
||
});
|
||
|
||
// Apply all bindings for each tool in a single call
|
||
Object.entries(toolBindings).forEach(([tool, bindings]) => {
|
||
toolGroup.setToolActive(tool, { bindings });
|
||
});
|
||
|
||
//// Always re-apply the mouse wheel binding for StackScrollTool
|
||
//toolGroup.setToolActive(StackScrollTool.toolName, {
|
||
// bindings: [
|
||
// {
|
||
// mouseButton: MouseBindings.Wheel, // Wheel Mouse
|
||
// },
|
||
// ],
|
||
//});
|
||
|
||
}
|
||
});
|
||
}, [mouseToolBindings, ctrlMouseToolBindings]);
|
||
|
||
const [sideMenuOpen, setSideMenuOpen] = useState(false);
|
||
|
||
// Add a ref to store selected files
|
||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||
|
||
// Handler for file input change
|
||
const handleFilesSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const files = event.target.files;
|
||
if (!files || files.length === 0) return;
|
||
|
||
// Reset order by slice location when new files are loaded
|
||
setOrderBySliceLocation(false);
|
||
|
||
// Convert FileList to Array and sort by name (optional, for series order)
|
||
const fileArray = Array.from(files).sort((a, b) => a.name.localeCompare(b.name));
|
||
|
||
// Create imageIds for each file using the cornerstone-wado-image-loader file API
|
||
// (Assumes cornerstone-wado-image-loader is configured for file loading)
|
||
const imageIds = fileArray.map(
|
||
file => `wadouri:${URL.createObjectURL(file)}`
|
||
);
|
||
|
||
setLoadedImageIdsAndCacheMeta(imageIds);
|
||
};
|
||
|
||
|
||
const handleToolChange = (mouseButton: string, toolName: string) => {
|
||
setMouseToolBindings(prev => ({ ...prev, [mouseButton]: toolName }))
|
||
}
|
||
|
||
// Update overlay info for a specific viewport
|
||
const updateOverlay = useCallback((viewportIdx: number, viewport: any) => {
|
||
if (!viewport) return;
|
||
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
||
? viewport.getCurrentImageIdIndex()
|
||
: 0;
|
||
const imageIds = typeof viewport.getImageIds === "function"
|
||
? viewport.getImageIds()
|
||
: [];
|
||
const total = imageIds.length;
|
||
|
||
let windowCenter = "";
|
||
let windowWidth = "";
|
||
if (typeof viewport.getProperties === "function") {
|
||
const props = viewport.getProperties();
|
||
if (props.voiRange) {
|
||
const { lower, upper } = props.voiRange;
|
||
windowCenter = ((upper + lower) / 2).toFixed(0);
|
||
windowWidth = (upper - lower).toFixed(0);
|
||
}
|
||
}
|
||
|
||
setImageInfos(prev => {
|
||
const next = [...prev];
|
||
next[viewportIdx] = {
|
||
index: currentIndex + 1,
|
||
total,
|
||
windowCenter,
|
||
windowWidth,
|
||
};
|
||
return next;
|
||
});
|
||
}, []);
|
||
|
||
const [gridBtnActive, setGridBtnActive] = useState(false);
|
||
|
||
function saveVisibleViewportState() {
|
||
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||
const viewportId = `CT_${i}`;
|
||
const renderingEngine = renderingEngineRef.current;
|
||
if (renderingEngine) {
|
||
const viewport = renderingEngine.getViewport(viewportId);
|
||
if (viewport && typeof viewport.getProperties === "function") {
|
||
prevViewportProps.current[i] = viewport.getProperties();
|
||
}
|
||
if (viewport && typeof viewport.getCurrentImageIdIndex === "function") {
|
||
prevScrollIndices.current[i] = viewport.getCurrentImageIdIndex();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
const setup = async () => {
|
||
// 1. Initialize Cornerstone3D and tools ONCE
|
||
if (!running.current) {
|
||
running.current = true;
|
||
await csRenderInit();
|
||
await csToolsInit(
|
||
{ showSVGCursors: true, }
|
||
);
|
||
dicomImageLoaderInit({ maxWebWorkers: 1 });
|
||
}
|
||
|
||
|
||
// 3. Get or create the rendering engine
|
||
let renderingEngine = renderingEngineRef.current;
|
||
if (!renderingEngine) {
|
||
renderingEngine = new RenderingEngine(renderingEngineId);
|
||
renderingEngineRef.current = renderingEngine;
|
||
}
|
||
|
||
// --- In your setup function, use the correct tool group per viewport ---
|
||
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||
if (!mainToolGroup) {
|
||
mainToolGroup = setupTools().mainToolGroup;
|
||
}
|
||
|
||
|
||
const visibleVolumeViewports = viewportModes
|
||
.slice(0, viewportGrid.rows * viewportGrid.cols)
|
||
.filter(mode => mode === 'volume').length;
|
||
|
||
// --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group ---
|
||
updateActiveReferenceLineViewport(mainToolGroup)
|
||
|
||
// 6. Enable and configure each viewport
|
||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||
|
||
let imageIds = viewportImageIds[i];
|
||
if (!imageIds || imageIds.length === 0) continue;
|
||
|
||
const viewportId = `CT_${i}`;
|
||
const element = viewportRefs.current[i];
|
||
if (!element) continue;
|
||
|
||
const viewportType =
|
||
viewportModes[i] === 'volume'
|
||
? Enums.ViewportType.ORTHOGRAPHIC // or Enums.ViewportType.VOLUME if supported
|
||
: Enums.ViewportType.STACK;
|
||
|
||
// Choose tool group based on viewport type
|
||
//const toolGroup = viewportModes[i] === 'volume' ? volumeToolGroup : stackToolGroup;
|
||
|
||
|
||
|
||
if (!element._cornerstoneEnabled || element._lastViewportType !== viewportType) {
|
||
if (renderingEngine.getViewport(viewportId)) {
|
||
renderingEngine.disableElement(viewportId);
|
||
}
|
||
renderingEngine.enableElement({
|
||
viewportId,
|
||
type: viewportType,
|
||
element,
|
||
});
|
||
element._cornerstoneEnabled = true;
|
||
element._lastViewportType = viewportType;
|
||
//mainToolGroup.addViewport(viewportId, renderingEngineId);
|
||
//toolGroup.addViewport(viewportId, renderingEngineId);
|
||
}
|
||
|
||
|
||
const viewport = renderingEngine.getViewport(viewportId);
|
||
if (!viewport) continue;
|
||
|
||
if (!element._resizeObserver) {
|
||
const resizeObserver = new window.ResizeObserver(() => {
|
||
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 (imageIds) {
|
||
viewport.setStack(imageIds);
|
||
}
|
||
// CrosshairsTool is not applicable for stack viewports
|
||
if (!crossHairsEnabled) {
|
||
mainToolGroup.addViewport(viewportId, renderingEngineId);
|
||
} else {
|
||
mainToolGroup.removeViewports(renderingEngineId, viewportId);
|
||
}
|
||
|
||
// Restore scroll, properties, etc.
|
||
} else if (viewportModes[i] === 'volume') {
|
||
mainToolGroup.addViewport(viewportId, renderingEngineId);
|
||
|
||
//let sortedImageIds = imageIds;
|
||
//if (imageIds && imageIds.length > 1) {
|
||
// sortedImageIds = [...imageIds].sort((a, b) => {
|
||
// const metaA = metaData.get('imagePlaneModule', a);
|
||
// const metaB = metaData.get('imagePlaneModule', b);
|
||
// const sliceA = metaA && metaA.sliceLocation !== undefined ? Number(metaA.sliceLocation) : 0;
|
||
// const sliceB = metaB && metaB.sliceLocation !== undefined ? Number(metaB.sliceLocation) : 0;
|
||
// return sliceA - sliceB;
|
||
// });
|
||
//}
|
||
|
||
// Use a unique volumeId per viewport
|
||
const volumeId = `myVolumeId_${i}`;
|
||
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds });
|
||
volume.load();
|
||
viewport.setVolumes([{ volumeId }]);
|
||
viewport.render();
|
||
continue
|
||
}
|
||
viewport.render();
|
||
|
||
// Restore previous scroll position for existing viewports only
|
||
if (
|
||
prevScrollIndices.current[i] !== undefined &&
|
||
typeof viewport.setImageIdIndex === "function"
|
||
) {
|
||
//viewport.setImageIdIndex(prevScrollIndices.current[i]);
|
||
csUtilities.jumpToSlice(viewport.element, { imageIndex: prevScrollIndices.current[i] })
|
||
}
|
||
|
||
|
||
// Restore previous state for existing viewports only
|
||
const prevProps = prevViewportProps.current[i];
|
||
if (prevProps && typeof viewport.setProperties === "function") {
|
||
// Only restore safe properties
|
||
const { voiRange, invert, interpolationType, VOILUTFunction, colormap } = prevProps;
|
||
const propsToRestore: any = {};
|
||
if (voiRange && voiRange.lower !== undefined && voiRange.upper !== undefined) propsToRestore.voiRange = voiRange;
|
||
if (invert !== undefined) propsToRestore.invert = invert;
|
||
if (interpolationType !== undefined) propsToRestore.interpolationType = interpolationType;
|
||
if (VOILUTFunction) propsToRestore.VOILUTFunction = VOILUTFunction;
|
||
if (
|
||
colormap !== undefined &&
|
||
colormap !== null &&
|
||
(typeof colormap === "string" || (typeof colormap === "object" && !("actor" in colormap)))
|
||
) {
|
||
propsToRestore.colormap = colormap;
|
||
}
|
||
setTimeout(() => {
|
||
viewport.setProperties(propsToRestore);
|
||
viewport.render()
|
||
}, 100);
|
||
}
|
||
|
||
|
||
|
||
// Always (re-)attach overlay update for each viewport
|
||
if (element._overlayListener) {
|
||
// Remove previous listener if present
|
||
viewport.element.removeEventListener(IMAGE_RENDERED, element._overlayListener);
|
||
}
|
||
const overlayListener = () => updateOverlay(i, viewport);
|
||
viewport.element.addEventListener(IMAGE_RENDERED, overlayListener);
|
||
element._overlayListener = overlayListener;
|
||
updateOverlay(i, viewport);
|
||
}
|
||
|
||
const activeVolumeViewports = [];
|
||
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||
const viewportId = `CT_${i}`;
|
||
const viewport = renderingEngineRef.current?.getViewport(viewportId);
|
||
if (viewport && viewportModes[i] === 'volume') {
|
||
activeVolumeViewports.push(viewport);
|
||
}
|
||
}
|
||
// Find all active volume viewports
|
||
const activeVolumeViewportIds = [];
|
||
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||
if (viewportModes[i] === 'volume') {
|
||
activeVolumeViewportIds.push(`CT_${i}`);
|
||
}
|
||
}
|
||
|
||
if (mainToolGroup && crossHairsEnabled) {
|
||
// Only enable CrosshairsTool if at least two volume viewports are active
|
||
if (activeVolumeViewportIds.length >= 2) {
|
||
// Make sure CrosshairsTool is added to the tool group
|
||
if (!mainToolGroup.hasTool(CrosshairsTool.toolName)) {
|
||
mainToolGroup.addTool(CrosshairsTool.toolName);
|
||
}
|
||
mainToolGroup.setToolEnabled(CrosshairsTool.toolName);
|
||
mainToolGroup.setToolConfiguration(CrosshairsTool.toolName, {
|
||
// Example: color: '#ffeb3b', lineWidth: 2
|
||
});
|
||
mainToolGroup.setToolActive(CrosshairsTool.toolName, {
|
||
bindings: [{ mouseButton: MouseBindings.Primary }],
|
||
});
|
||
|
||
} else {
|
||
// Disable CrosshairsTool if not enough volume viewports
|
||
if (mainToolGroup.hasTool(CrosshairsTool.toolName)) {
|
||
mainToolGroup.setToolDisabled(CrosshairsTool.toolName);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 7. Apply mouse tool bindings
|
||
applyMouseToolBindings();
|
||
};
|
||
|
||
// Use requestAnimationFrame to ensure DOM is painted before setup
|
||
const raf = requestAnimationFrame(() => {
|
||
setup();
|
||
});
|
||
|
||
// Cleanup: disconnect ResizeObservers
|
||
return () => {
|
||
viewportRefs.current.forEach(el => {
|
||
if (el && el._resizeObserver) {
|
||
el._resizeObserver.disconnect();
|
||
delete el._resizeObserver;
|
||
}
|
||
});
|
||
cancelAnimationFrame(raf);
|
||
};
|
||
}, [elementRef, updateOverlay, viewportGrid, viewportModes, viewportImageIds]);
|
||
|
||
|
||
// Handler to reset the view (camera and window/level)
|
||
const handleDoubleClick = useCallback((viewportIdx: number) => {
|
||
const viewportId = `CT_${viewportIdx}`;
|
||
const renderingEngine = renderingEngineRef.current;
|
||
const viewport = renderingEngine.getViewport(viewportId);
|
||
if (viewport && typeof viewport.resetCamera === "function") {
|
||
viewport.resetCamera();
|
||
viewport.resetProperties(); // Resets window/level and other properties
|
||
viewport.render();
|
||
}
|
||
}, [])
|
||
|
||
// Handler to apply a preset for a specific viewport
|
||
const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => {
|
||
const viewportId = `CT_${viewportIdx}`;
|
||
try {
|
||
const renderingEngine = renderingEngineRef.current;
|
||
if (renderingEngine) {
|
||
const viewport = renderingEngine.getViewport(viewportId);
|
||
if (viewport && typeof viewport.setProperties === "function") {
|
||
viewport.setProperties({
|
||
voiRange: {
|
||
lower: center - width / 2,
|
||
upper: center + width / 2,
|
||
},
|
||
});
|
||
viewport.render();
|
||
updateOverlay(viewportIdx, viewport);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Ignore if not found
|
||
}
|
||
}, [updateOverlay]);
|
||
|
||
// Update overlay and preset state arrays when grid changes
|
||
useEffect(() => {
|
||
setImageInfos(Array(viewportGrid.rows * viewportGrid.cols).fill({
|
||
index: 0,
|
||
total: 0,
|
||
windowCenter: "",
|
||
windowWidth: "",
|
||
}));
|
||
setPresetsOpenArr(Array(viewportGrid.rows * viewportGrid.cols).fill(false));
|
||
}, [viewportGrid]);
|
||
|
||
// Re-apply bindings when tool sets or ctrl changes
|
||
useEffect(() => {
|
||
applyMouseToolBindings()
|
||
}, [mouseToolBindings, ctrlMouseToolBindings, ctrlPressed, applyMouseToolBindings])
|
||
|
||
// Close the menu when clicking outside
|
||
useEffect(() => {
|
||
if (!presetsOpen) return;
|
||
const handleClick = (e: MouseEvent) => {
|
||
const menu = document.querySelector(".preset-menu");
|
||
if (menu && !menu.contains(e.target as Node)) {
|
||
setPresetsOpen(false);
|
||
}
|
||
};
|
||
document.addEventListener("mousedown", handleClick);
|
||
return () => document.removeEventListener("mousedown", handleClick);
|
||
}, [presetsOpen]);
|
||
|
||
useEffect(() => {
|
||
const handleContextMenu = (e: MouseEvent) => {
|
||
e.preventDefault();
|
||
};
|
||
document.addEventListener("contextmenu", handleContextMenu);
|
||
return () => {
|
||
document.removeEventListener("contextmenu", handleContextMenu);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!referenceLinesEnabled) return;
|
||
const mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||
updateActiveReferenceLineViewport(mainToolGroup)
|
||
}, [activeViewport, referenceLinesEnabled]);
|
||
|
||
useEffect(() => {
|
||
if (!orderBySliceLocation) return;
|
||
|
||
// Only sort if all metadata is loaded for all viewports
|
||
const allLoaded = viewportImageIds.every(imageIds =>
|
||
!imageIds || imageIds.length === 0 ||
|
||
imageIds.every(id => !!metaData.get("imagePlaneModule", id))
|
||
);
|
||
if (!allLoaded) return;
|
||
|
||
setViewportImageIds(prev => {
|
||
const next = prev.map(imageIds => {
|
||
if (!imageIds || imageIds.length === 0) return imageIds;
|
||
|
||
const withSliceLoc = imageIds.map(id => ({
|
||
imageId: id,
|
||
sliceLoc: (() => {
|
||
const meta = metaData.get('imagePlaneModule', id);
|
||
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
|
||
})(),
|
||
}));
|
||
|
||
const sorted = [...withSliceLoc].sort((a, b) => {
|
||
if (a.sliceLoc !== null && b.sliceLoc !== null) {
|
||
return a.sliceLoc - b.sliceLoc;
|
||
}
|
||
return a.imageId.localeCompare(b.imageId);
|
||
});
|
||
|
||
const sortedIds = sorted.map(obj => obj.imageId);
|
||
if (JSON.stringify(sortedIds) === JSON.stringify(imageIds)) {
|
||
return imageIds;
|
||
}
|
||
return sortedIds;
|
||
});
|
||
return next;
|
||
});
|
||
}, [orderBySliceLocation, availableStacks]);
|
||
|
||
|
||
function updateActiveReferenceLineViewport(mainToolGroup: cornerstoneTools.Types.IToolGroup) {
|
||
// Ensure the main tool group is defined (it won't be during setup)
|
||
if (!mainToolGroup) return;
|
||
const sourceViewportId = `CT_${activeViewport}`
|
||
if (referenceLinesEnabled) {
|
||
mainToolGroup.setToolEnabled(ReferenceLinesTool.toolName)
|
||
mainToolGroup.setToolConfiguration(ReferenceLinesTool.toolName, {
|
||
sourceViewportId,
|
||
})
|
||
} else {
|
||
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
|
||
}
|
||
}
|
||
|
||
const viewports = [];
|
||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||
// Determine if this viewport should be visible in the current grid
|
||
const isVisible = i < viewportGrid.rows * viewportGrid.cols;
|
||
|
||
viewports.push(
|
||
<div
|
||
key={i}
|
||
ref={el => (viewportRefs.current[i] = el)}
|
||
style={{
|
||
flex: 1,
|
||
border: "1px solid #222",
|
||
background: "#000",
|
||
minWidth: 0,
|
||
minHeight: 0,
|
||
position: "relative",
|
||
overflow: "hidden",
|
||
flexDirection: "column",
|
||
gridRow: Math.floor(i / viewportGrid.cols) + 1,
|
||
gridColumn: (i % viewportGrid.cols) + 1,
|
||
height: "100%",
|
||
visibility: isVisible ? "visible" : "hidden",
|
||
pointerEvents: isVisible ? "auto" : "none",
|
||
// Remove outline, use only boxShadow for indicator
|
||
boxShadow: activeViewport === i
|
||
? "0 0 0 4px #1976d2, 0 0 16px 4px #1976d2cc"
|
||
: undefined,
|
||
cursor: "pointer",
|
||
zIndex: activeViewport === i ? 10 : 1,
|
||
}}
|
||
onClick={() => setActiveViewport(i)}
|
||
onDoubleClick={() => handleDoubleClick(i)}
|
||
>
|
||
<button
|
||
style={{
|
||
position: "absolute",
|
||
top: 8,
|
||
left: 8,
|
||
zIndex: 20,
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
padding: "4px 8px",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
opacity: 0.85,
|
||
}}
|
||
onClick={() => {
|
||
setViewportModes(prev => {
|
||
const next = [...prev];
|
||
next[i] = prev[i] === 'stack' ? 'volume' : 'stack';
|
||
return next;
|
||
});
|
||
}}
|
||
>
|
||
{viewportModes[i] === 'stack' ? 'Volume' : 'Stack'}
|
||
</button>
|
||
{viewportModes[i] === 'volume' && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 8,
|
||
left: 70,
|
||
zIndex: 21,
|
||
display: "flex",
|
||
gap: "6px",
|
||
background: "rgba(34,34,34,0.85)",
|
||
borderRadius: "4px",
|
||
padding: "2px 6px",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<button
|
||
style={{
|
||
background: "#444",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "3px",
|
||
padding: "2px 8px",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
marginRight: "2px",
|
||
}}
|
||
onClick={() => {
|
||
const renderingEngine = renderingEngineRef.current;
|
||
const viewport = renderingEngine?.getViewport(`CT_${i}`);
|
||
if (viewport && viewport.setOrientation) {
|
||
viewport.setOrientation("axial");
|
||
viewport.render();
|
||
}
|
||
}}
|
||
>
|
||
Axial
|
||
</button>
|
||
<button
|
||
style={{
|
||
background: "#444",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "3px",
|
||
padding: "2px 8px",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
marginRight: "2px",
|
||
}}
|
||
onClick={() => {
|
||
const renderingEngine = renderingEngineRef.current;
|
||
const viewport = renderingEngine?.getViewport(`CT_${i}`);
|
||
if (viewport && viewport.setOrientation) {
|
||
viewport.setOrientation("sagittal");
|
||
viewport.render();
|
||
}
|
||
}}
|
||
>
|
||
Sagittal
|
||
</button>
|
||
<button
|
||
style={{
|
||
background: "#444",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "3px",
|
||
padding: "2px 8px",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
}}
|
||
onClick={() => {
|
||
const renderingEngine = renderingEngineRef.current;
|
||
const viewport = renderingEngine?.getViewport(`CT_${i}`);
|
||
if (viewport && viewport.setOrientation) {
|
||
viewport.setOrientation("coronal");
|
||
viewport.render();
|
||
}
|
||
}}
|
||
>
|
||
Coronal
|
||
</button>
|
||
</div>
|
||
)}
|
||
{/* Overlay info (bottom left of viewport) */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: 8,
|
||
bottom: 8,
|
||
background: "rgba(0,0,0,0.7)",
|
||
color: "#fff",
|
||
padding: "6px 12px",
|
||
borderRadius: "4px",
|
||
fontSize: "14px",
|
||
zIndex: 10,
|
||
pointerEvents: "none",
|
||
minWidth: "120px",
|
||
userSelect: "none",
|
||
}}
|
||
>
|
||
{viewportModes[i] === "stack" ? (
|
||
<>
|
||
<div>
|
||
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
||
</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>
|
||
|
||
{/* Preset menu (bottom right of viewport) */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
right: 8,
|
||
bottom: 8,
|
||
zIndex: 10,
|
||
}}
|
||
>
|
||
<div
|
||
className="preset-menu"
|
||
style={{
|
||
background: "rgba(0,0,0,0.7)",
|
||
color: "#fff",
|
||
borderRadius: "4px",
|
||
fontSize: "14px",
|
||
minWidth: "120px",
|
||
overflow: "hidden",
|
||
cursor: "pointer",
|
||
position: "relative",
|
||
boxShadow: presetsOpenArr[i] ? "0 2px 8px rgba(0,0,0,0.3)" : undefined,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: "6px 12px",
|
||
fontWeight: "bold",
|
||
userSelect: "none",
|
||
}}
|
||
onClick={() =>
|
||
setPresetsOpenArr(prev => {
|
||
const next = [...prev];
|
||
next[i] = !next[i];
|
||
return next;
|
||
})
|
||
}
|
||
>
|
||
Presets
|
||
<span style={{ float: "right", fontWeight: "normal" }}>
|
||
{presetsOpenArr[i] ? "▲" : "▼"}
|
||
</span>
|
||
</div>
|
||
{presetsOpenArr[i] && (
|
||
<div
|
||
className="preset-menu-content"
|
||
style={{
|
||
background: "rgba(0,0,0,0.95)",
|
||
borderRadius: "0 0 4px 4px",
|
||
padding: "6px 0 6px 0",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
zIndex: 11,
|
||
}}
|
||
>
|
||
{WINDOW_LEVEL_PRESETS.map((preset) => (
|
||
<button
|
||
key={preset.name}
|
||
style={{
|
||
display: "block",
|
||
width: "100%",
|
||
margin: "2px 0",
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "3px",
|
||
cursor: "pointer",
|
||
padding: "4px 0",
|
||
fontSize: "13px",
|
||
opacity: 0.9,
|
||
}}
|
||
onClick={() => {
|
||
handleApplyPreset(i, preset.center, preset.width);
|
||
setPresetsOpenArr(prev => {
|
||
const next = [...prev];
|
||
next[i] = false;
|
||
return next;
|
||
});
|
||
}}
|
||
>
|
||
{preset.name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{/* Position bar inside each viewport */}
|
||
{viewportModes[i] === "stack" && viewportImageIds[i] && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: "10%",
|
||
right: 8,
|
||
width: "12px",
|
||
height: "80%",
|
||
maxHeight: "100%",
|
||
minHeight: 0,
|
||
zIndex: 30,
|
||
display: viewportImageIds[i].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",
|
||
}}
|
||
>
|
||
{viewportImageIds[i].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 / viewportImageIds[i].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 < viewportImageIds[i].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);
|
||
csUtilities.jumpToSlice(viewport.element, { imageIndex: idx })
|
||
//viewport.render();
|
||
updateOverlay(i, viewport);
|
||
|
||
}
|
||
}}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: 8,
|
||
right: 8,
|
||
bottom: 8,
|
||
zIndex: 20,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "flex-end",
|
||
gap: "8px",
|
||
background: "rgba(34,34,34,0.85)",
|
||
borderRadius: "4px",
|
||
padding: "2px 8px",
|
||
}}
|
||
>
|
||
<select
|
||
value={selectedStackIdx[i] || 0}
|
||
onChange={e => handleLoadStack(i, Number(e.target.value))}
|
||
style={{
|
||
background: "#222",
|
||
color: "#fff",
|
||
border: "1px solid #444",
|
||
borderRadius: "4px",
|
||
padding: "2px 8px",
|
||
fontSize: "13px",
|
||
marginRight: "6px",
|
||
}}
|
||
>
|
||
{availableStacks.map((stack, idx) => (
|
||
<option key={idx} value={idx}>
|
||
Stack {idx + 1} ({stack.length} images)
|
||
</option>
|
||
))}
|
||
</select>
|
||
<button
|
||
style={{
|
||
background: "#1976d2",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "3px",
|
||
padding: "2px 12px",
|
||
cursor: "pointer",
|
||
fontSize: "13px",
|
||
}}
|
||
onClick={() => handleLoadStack(i, selectedStackIdx[i] || 0)}
|
||
disabled={!availableStacks.length}
|
||
>
|
||
Load Stack
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
return (
|
||
<div style={{
|
||
position: "relative",
|
||
width: "99vw",
|
||
height: "99vh",
|
||
background: "#111",
|
||
padding: "12px", // <-- Add this line
|
||
boxSizing: "border-box", // <-- Ensure padding doesn't shrink content
|
||
background: "#222",
|
||
// overflow: "hidden",
|
||
}}>
|
||
<div>
|
||
{/* Side menu toggle button */}
|
||
<button
|
||
style={{
|
||
position: "absolute",
|
||
top: "50%",
|
||
transform: "translateY(-50%)",
|
||
left: 20,
|
||
zIndex: 200,
|
||
width: 40,
|
||
height: 40,
|
||
background: "#222",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "50%",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "22px",
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
||
transition: "background 0.2s",
|
||
}}
|
||
onClick={() => setSideMenuOpen(open => !open)}
|
||
aria-label="Open menu"
|
||
>
|
||
☰
|
||
</button>
|
||
|
||
{/* Side menu drawer */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: sideMenuOpen ? 0 : -220,
|
||
width: 220,
|
||
height: "100%",
|
||
background: "#222",
|
||
color: "#fff",
|
||
zIndex: 199,
|
||
boxShadow: sideMenuOpen ? "2px 0 12px rgba(0,0,0,0.3)" : "none",
|
||
transition: "left 0.25s cubic-bezier(.4,2,.6,1)",
|
||
flexDirection: "column",
|
||
padding: "24px 16px 16px 16px",
|
||
display: sideMenuOpen ? "flex" : "none",
|
||
}}
|
||
>
|
||
<div style={{ fontWeight: "bold", fontSize: 18, marginBottom: 24 }}>
|
||
Menu
|
||
<button
|
||
style={{
|
||
float: "right",
|
||
background: "transparent",
|
||
color: "#fff",
|
||
border: "none",
|
||
fontSize: "22px",
|
||
cursor: "pointer",
|
||
}}
|
||
onClick={() => setSideMenuOpen(false)}
|
||
aria-label="Close menu"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<button
|
||
style={{
|
||
padding: "10px 18px",
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
marginBottom: "18px",
|
||
width: "100%",
|
||
}}
|
||
onClick={() => fileInputRef.current?.click()}
|
||
>
|
||
Load DICOM Folder
|
||
</button>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
style={{ display: "none" }}
|
||
multiple
|
||
webkitdirectory="true"
|
||
directory="true"
|
||
onChange={handleFilesSelected}
|
||
accept=".dcm,application/dicom"
|
||
/>
|
||
<div
|
||
style={{
|
||
marginBottom: "18px",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "8px",
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
id="orderBySliceLocation"
|
||
checked={orderBySliceLocation}
|
||
onChange={e => setOrderBySliceLocation(e.target.checked)}
|
||
style={{ accentColor: "#666" }}
|
||
/>
|
||
<label htmlFor="orderBySliceLocation" style={{ fontSize: "15px", cursor: "pointer" }}>
|
||
Order by Slice Location
|
||
</label>
|
||
</div>
|
||
<button
|
||
style={{
|
||
padding: "10px 18px",
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
marginBottom: "18px",
|
||
width: "100%",
|
||
}}
|
||
onClick={handleShowMetadata}
|
||
>
|
||
Show DICOM Metadata
|
||
</button>
|
||
<button
|
||
style={{
|
||
padding: "10px 18px",
|
||
background: crossHairsEnabled ? "#1976d2" : "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
marginBottom: "18px",
|
||
width: "100%",
|
||
}}
|
||
onClick={() => setCrossHairsEnabled((prev) => !prev)}
|
||
>
|
||
{crossHairsEnabled ? "Disable" : "Enable"} Cross Reference Lines
|
||
</button>
|
||
<button
|
||
style={{
|
||
padding: "10px 18px",
|
||
background: referenceLinesEnabled ? "#1976d2" : "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
marginBottom: "18px",
|
||
width: "100%",
|
||
}}
|
||
onClick={() => setReferenceLinesEnabled((prev) => !prev)}
|
||
>
|
||
{referenceLinesEnabled ? "Disable" : "Enable"} Reference Lines
|
||
</button>
|
||
<button
|
||
onClick={sortViewportImageIdsBySliceLocation}
|
||
style={{ margin: 8, padding: 8 }}
|
||
>
|
||
Sort All Viewports by Slice Location
|
||
</button>
|
||
<button
|
||
style={{
|
||
padding: "10px 18px",
|
||
background: altEnablesReferenceCursors ? "#1976d2" : "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
marginBottom: "18px",
|
||
width: "100%",
|
||
}}
|
||
onClick={() => setAltEnablesReferenceCursors((prev) => !prev)}
|
||
>
|
||
{altEnablesReferenceCursors
|
||
? "Disable Alt Key for Reference Cursors"
|
||
: "Enable Alt Key for Reference Cursors"}
|
||
</button>
|
||
{/* Add more menu items here if needed */}
|
||
|
||
</div>
|
||
</div>
|
||
{/* Viewport grid menu at top center */}
|
||
|
||
|
||
<div
|
||
className="grid-menu-hover-container"
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
zIndex: 100,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
alignItems: "center",
|
||
width: "auto",
|
||
height: "48px",
|
||
pointerEvents: "none", // Only the hit area and button will be interactive
|
||
}}
|
||
>
|
||
{/* Transparent hit area to catch hover/focus */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
width: 80,
|
||
height: 14,
|
||
zIndex: 1,
|
||
pointerEvents: "auto",
|
||
background: "transparent",
|
||
}}
|
||
onMouseEnter={() => setGridBtnActive(true)}
|
||
onFocus={() => setGridBtnActive(true)}
|
||
tabIndex={-1}
|
||
/>
|
||
<button
|
||
className="grid-menu-btn"
|
||
style={{
|
||
position: "relative",
|
||
top: gridBtnActive ? "0" : "-22px",
|
||
opacity: gridBtnActive ? 1 : 0.5,
|
||
transition: "top 0.25s cubic-bezier(.4,2,.6,1), opacity 0.2s",
|
||
padding: "6px 16px",
|
||
background: "#222",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
marginRight: "8px",
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
||
pointerEvents: gridBtnActive ? "auto" : "none", // Only clickable when active
|
||
zIndex: 2,
|
||
}}
|
||
onMouseLeave={() => setGridBtnActive(false)}
|
||
onBlur={() => setGridBtnActive(false)}
|
||
onClick={() => setViewportMenuOpen(open => !open)}
|
||
tabIndex={0}
|
||
>
|
||
Grid: {viewportGrid.rows}x{viewportGrid.cols} ▼
|
||
</button>
|
||
{viewportMenuOpen && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: "110%",
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
background: "#222",
|
||
color: "#fff",
|
||
borderRadius: "6px",
|
||
boxShadow: "0 2px 12px rgba(0,0,0,0.4)",
|
||
padding: "12px",
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(3, 40px)",
|
||
gap: "8px",
|
||
zIndex: 200,
|
||
pointerEvents: "auto",
|
||
}}
|
||
>
|
||
{gridOptions.map(opt => (
|
||
<button
|
||
key={`${opt.rows}x${opt.cols}`}
|
||
style={{
|
||
width: "40px",
|
||
height: "40px",
|
||
background: (opt.rows === viewportGrid.rows && opt.cols === viewportGrid.cols) ? "#444" : "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
fontWeight: "bold",
|
||
cursor: "pointer",
|
||
fontSize: "15px",
|
||
}}
|
||
onClick={() => {
|
||
saveVisibleViewportState(viewportGrid, renderingEngineRef.current);
|
||
setViewportGrid({ rows: opt.rows, cols: opt.cols })
|
||
setViewportMenuOpen(false)
|
||
}}
|
||
>
|
||
{opt.rows}x{opt.cols}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
|
||
{/* Viewport grid (inside main div) */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
width: "100%",
|
||
height: "100%",
|
||
display: "grid",
|
||
margin: 3,
|
||
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
|
||
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
|
||
gap: "5px", // Increase gap for better visibility
|
||
zIndex: 1,
|
||
}}
|
||
>
|
||
{viewports}
|
||
|
||
|
||
</div>
|
||
|
||
|
||
{/* Settings button at top right */}
|
||
<button
|
||
style={{
|
||
position: "absolute",
|
||
top: 8,
|
||
right: 8,
|
||
zIndex: 20,
|
||
padding: "6px 12px",
|
||
background: "#222",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
cursor: "pointer",
|
||
opacity: 0.85,
|
||
userSelect: "none",
|
||
}}
|
||
onClick={() => setSettingsOpen(true)}
|
||
>
|
||
Settings
|
||
</button>
|
||
|
||
{/* Settings Modal (viewport-local, not fixed to page) */}
|
||
{
|
||
settingsOpen && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
width: "100%",
|
||
height: "100%",
|
||
background: "rgba(0,0,0,0.4)",
|
||
zIndex: 1000,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
onClick={() => setSettingsOpen(false)}
|
||
>
|
||
<div
|
||
style={{
|
||
background: "#222",
|
||
color: "#fff",
|
||
borderRadius: "8px",
|
||
minWidth: "320px",
|
||
minHeight: "180px",
|
||
padding: "24px",
|
||
boxShadow: "0 4px 24px rgba(0,0,0,0.4)",
|
||
position: "relative",
|
||
}}
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
<div style={{ fontSize: "20px", marginBottom: "16px" }}>
|
||
Settings
|
||
</div>
|
||
{/* Settings content: Tool dropdowns */}
|
||
<div style={{ marginBottom: "24px" }}>
|
||
<div style={{ marginBottom: 12, fontWeight: "bold" }}>Mouse Button Tool Bindings</div>
|
||
{MOUSE_BUTTONS.map((btn) => (
|
||
<div key={btn.value} style={{ marginBottom: 10 }}>
|
||
<label style={{ marginRight: 8 }}>{btn.label} Button:</label>
|
||
<select
|
||
value={mouseToolBindings[btn.value]}
|
||
onChange={e => setMouseToolBindings(prev => ({ ...prev, [btn.value]: e.target.value }))}
|
||
style={{
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "1px solid #444",
|
||
borderRadius: "4px",
|
||
padding: "4px 8px",
|
||
fontSize: "14px",
|
||
}}
|
||
>
|
||
{TOOL_OPTIONS.map(opt => (
|
||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
))}
|
||
|
||
{/* Expandable advanced mouse bindings */}
|
||
<div style={{ margin: "12px 0" }}>
|
||
<button
|
||
style={{
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
padding: "4px 12px",
|
||
cursor: "pointer",
|
||
fontSize: "14px",
|
||
marginBottom: "8px",
|
||
}}
|
||
onClick={() => setShowAdvancedMouseBindings(v => !v)}
|
||
>
|
||
{showAdvancedMouseBindings ? "Hide" : "Show"} Advanced Mouse Bindings
|
||
</button>
|
||
{showAdvancedMouseBindings && (
|
||
<div style={{ marginTop: 8 }}>
|
||
{ALL_MOUSE_BINDINGS.slice(3).map((btn) => (
|
||
<div key={btn.value} style={{ marginBottom: 10 }}>
|
||
<label style={{ marginRight: 8 }}>{btn.label}:</label>
|
||
<select
|
||
value={mouseToolBindings[btn.value] || ""}
|
||
onChange={e => setMouseToolBindings(prev => ({ ...prev, [btn.value]: e.target.value }))}
|
||
style={{
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "1px solid #444",
|
||
borderRadius: "4px",
|
||
padding: "4px 8px",
|
||
fontSize: "14px",
|
||
}}
|
||
>
|
||
<option value="">(None)</option>
|
||
{TOOL_OPTIONS.map(opt => (
|
||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{/* Ctrl + Mouse Button Tool Bindings */}
|
||
<div style={{ margin: "18px 0 8px 0", fontWeight: "bold" }}>Ctrl + Mouse Button Tool Bindings</div>
|
||
{MOUSE_BUTTONS.map((btn) => (
|
||
<div key={btn.value + "_ctrl"} style={{ marginBottom: 10 }}>
|
||
<label style={{ marginRight: 8 }}>{btn.label} + Ctrl:</label>
|
||
<select
|
||
value={ctrlMouseToolBindings[btn.value] || ""}
|
||
onChange={e => setCtrlMouseToolBindings(prev => ({ ...prev, [btn.value]: e.target.value }))}
|
||
style={{
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "1px solid #444",
|
||
borderRadius: "4px",
|
||
padding: "4px 8px",
|
||
fontSize: "14px",
|
||
}}
|
||
>
|
||
<option value="">(None)</option>
|
||
{TOOL_OPTIONS.map(opt => (
|
||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
))}
|
||
|
||
{/* Expandable advanced ctrl+mouse bindings */}
|
||
{showAdvancedMouseBindings && (
|
||
<div style={{ marginTop: 8 }}>
|
||
{ALL_MOUSE_BINDINGS.slice(3).map((btn) => (
|
||
<div key={btn.value + "_ctrl"} style={{ marginBottom: 10 }}>
|
||
<label style={{ marginRight: 8 }}>{btn.label} + Ctrl:</label>
|
||
<select
|
||
value={ctrlMouseToolBindings[btn.value] || ""}
|
||
onChange={e => setCtrlMouseToolBindings(prev => ({ ...prev, [btn.value]: e.target.value }))}
|
||
style={{
|
||
background: "#333",
|
||
color: "#fff",
|
||
border: "1px solid #444",
|
||
borderRadius: "4px",
|
||
padding: "4px 8px",
|
||
fontSize: "14px",
|
||
}}
|
||
>
|
||
<option value="">(None)</option>
|
||
{TOOL_OPTIONS.map(opt => (
|
||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
style={{
|
||
position: "absolute",
|
||
top: 12,
|
||
right: 12,
|
||
background: "transparent",
|
||
color: "#fff",
|
||
border: "none",
|
||
fontSize: "20px",
|
||
cursor: "pointer",
|
||
}}
|
||
onClick={() => setSettingsOpen(false)}
|
||
aria-label="Close"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
{
|
||
metaModalOpen && (
|
||
<div
|
||
style={{
|
||
position: "fixed",
|
||
top: 0,
|
||
left: 0,
|
||
width: "100vw",
|
||
height: "100vh",
|
||
background: "rgba(0,0,0,0.5)",
|
||
zIndex: 2000,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
onClick={() => setMetaModalOpen(false)}
|
||
>
|
||
<div
|
||
style={{
|
||
background: "#222",
|
||
color: "#fff",
|
||
borderRadius: "8px",
|
||
minWidth: "320px",
|
||
maxWidth: "80vw",
|
||
maxHeight: "80vh",
|
||
padding: "24px",
|
||
boxShadow: "0 4px 24px rgba(0,0,0,0.4)",
|
||
position: "relative",
|
||
overflow: "auto",
|
||
}}
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
<div style={{ fontSize: "20px", marginBottom: "16px" }}>
|
||
DICOM Metadata
|
||
</div>
|
||
{metaModalContent}
|
||
<button
|
||
style={{
|
||
position: "absolute",
|
||
top: 12,
|
||
right: 12,
|
||
background: "transparent",
|
||
color: "#fff",
|
||
border: "none",
|
||
fontSize: "20px",
|
||
cursor: "pointer",
|
||
}}
|
||
onClick={() => setMetaModalOpen(false)}
|
||
aria-label="Close"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
{/* ...rest of your UI... */}
|
||
|
||
</div >
|
||
)
|
||
|
||
}
|
||
|
||
export default App
|
||
|
||
function setupTools() {
|
||
cornerstoneTools.addTool(PanTool)
|
||
cornerstoneTools.addTool(WindowLevelTool)
|
||
cornerstoneTools.addTool(StackScrollTool)
|
||
cornerstoneTools.addTool(ZoomTool)
|
||
cornerstoneTools.addTool(PlanarRotateTool)
|
||
cornerstoneTools.addTool(LengthTool)
|
||
cornerstoneTools.addTool(ProbeTool)
|
||
cornerstoneTools.addTool(ArrowAnnotateTool)
|
||
cornerstoneTools.addTool(RectangleROITool)
|
||
cornerstoneTools.addTool(EllipticalROITool)
|
||
cornerstoneTools.addTool(PlanarFreehandROITool)
|
||
cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool)
|
||
cornerstoneTools.addTool(SculptorTool)
|
||
cornerstoneTools.addTool(ReferenceCursors)
|
||
cornerstoneTools.addTool(CrosshairsTool)
|
||
cornerstoneTools.addTool(ReferenceLinesTool);
|
||
|
||
|
||
|
||
let mainToolGroup = ToolGroupManager.getToolGroup(MAIN_TOOL_GROUP_ID);
|
||
if (!mainToolGroup) {
|
||
mainToolGroup = ToolGroupManager.createToolGroup(MAIN_TOOL_GROUP_ID);
|
||
// Add all tools you need for both stack and volume
|
||
mainToolGroup.addTool(WindowLevelTool.toolName);
|
||
mainToolGroup.addTool(PanTool.toolName);
|
||
mainToolGroup.addTool(ZoomTool.toolName);
|
||
mainToolGroup.addTool(StackScrollTool.toolName, { loop: false });
|
||
mainToolGroup.addTool(PlanarRotateTool.toolName);
|
||
mainToolGroup.addTool(LengthTool.toolName);
|
||
mainToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' });
|
||
mainToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
|
||
mainToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
|
||
mainToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
|
||
mainToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
||
mainToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
||
mainToolGroup.addTool(SculptorTool.toolName, {});
|
||
//mainToolGroup.addTool(CrosshairsTool.toolName);
|
||
mainToolGroup.addTool(ReferenceLinesTool.toolName);
|
||
mainToolGroup.addTool(ReferenceCursors.toolName);
|
||
}
|
||
|
||
return { mainToolGroup };
|
||
}
|