lots of fixes
This commit is contained in:
+370
-285
@@ -6,7 +6,7 @@ import {
|
||||
} 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 { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool } from "@cornerstonejs/tools"
|
||||
import { LengthTool, ProbeTool, ArrowAnnotateTool, RectangleROITool, EllipticalROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, SculptorTool, CrosshairsTool, ReferenceLinesTool } 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';
|
||||
@@ -24,7 +24,6 @@ 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';
|
||||
|
||||
// Common CT presets (add more as needed)
|
||||
const WINDOW_LEVEL_PRESETS = [
|
||||
@@ -87,6 +86,7 @@ function App() {
|
||||
const [loadedImageIds, setLoadedImageIds] = useState<string[] | null>(null);
|
||||
|
||||
const [crossReferenceEnabled, setCrossReferenceEnabled] = useState(true);
|
||||
const [referenceLinesEnabled, setReferenceLinesEnabled] = useState(false);
|
||||
|
||||
// Wrap setLoadedImageIds to also cache DICOM metadata
|
||||
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[]) => {
|
||||
@@ -98,9 +98,9 @@ function App() {
|
||||
const url = imageId.slice(8);
|
||||
// Only cache if it's a blob or file URL
|
||||
if (url.startsWith("blob:") || url.startsWith("/")) {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||
//const response = await fetch(url);
|
||||
//const arrayBuffer = await response.arrayBuffer();
|
||||
//const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||
cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
||||
|
||||
@@ -251,32 +251,36 @@ function App() {
|
||||
};
|
||||
|
||||
// Helper to apply all mouse tool bindings (normal or ctrl)
|
||||
const applyMouseToolBindings = useCallback(() => {
|
||||
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId)
|
||||
const applyMouseToolBindings = useCallback(() => {
|
||||
// Apply to both stack and volume tool groups
|
||||
const stackToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
|
||||
const volumeToolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
|
||||
|
||||
[stackToolGroup, volumeToolGroup].forEach(toolGroup => {
|
||||
if (toolGroup) {
|
||||
// Set all tools to passive first, removing all bindings
|
||||
TOOL_OPTIONS.forEach(opt => {
|
||||
toolGroup.setToolPassive(opt.value, { removeAllBindings: true })
|
||||
})
|
||||
toolGroup.setToolPassive(opt.value, { removeAllBindings: true });
|
||||
});
|
||||
|
||||
// Collect all bindings for each tool
|
||||
const toolBindings: Record<string, Array<any>> = {}
|
||||
const toolBindings: Record<string, Array<any>> = {};
|
||||
|
||||
// Normal bindings
|
||||
Object.entries(mouseToolBindings).forEach(([btn, tool]) => {
|
||||
if (!toolBindings[tool]) toolBindings[tool] = []
|
||||
toolBindings[tool].push({ mouseButton: MouseBindings[btn] })
|
||||
})
|
||||
if (!toolBindings[tool]) toolBindings[tool] = [];
|
||||
toolBindings[tool].push({ mouseButton: MouseBindings[btn] });
|
||||
});
|
||||
// Ctrl bindings
|
||||
Object.entries(ctrlMouseToolBindings).forEach(([btn, tool]) => {
|
||||
if (!toolBindings[tool]) toolBindings[tool] = []
|
||||
toolBindings[tool].push({ mouseButton: MouseBindings[btn], modifierKey: KeyboardBindings.Ctrl })
|
||||
})
|
||||
if (!toolBindings[tool]) toolBindings[tool] = [];
|
||||
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 })
|
||||
})
|
||||
toolGroup.setToolActive(tool, { bindings });
|
||||
});
|
||||
|
||||
// Always re-apply the mouse wheel binding for StackScrollTool
|
||||
toolGroup.setToolActive(StackScrollTool.toolName, {
|
||||
@@ -285,9 +289,10 @@ function App() {
|
||||
mouseButton: MouseBindings.Wheel, // Wheel Mouse
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [mouseToolBindings, ctrlMouseToolBindings])
|
||||
});
|
||||
}, [mouseToolBindings, ctrlMouseToolBindings]);
|
||||
|
||||
const [sideMenuOpen, setSideMenuOpen] = useState(false);
|
||||
|
||||
@@ -353,6 +358,8 @@ function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [gridBtnActive, setGridBtnActive] = useState(false);
|
||||
|
||||
function saveVisibleViewportState() {
|
||||
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||||
const viewportId = `CT_${i}`;
|
||||
@@ -375,7 +382,9 @@ function App() {
|
||||
if (!running.current) {
|
||||
running.current = true;
|
||||
await csRenderInit();
|
||||
await csToolsInit();
|
||||
await csToolsInit(
|
||||
{ showSVGCursors: true, }
|
||||
);
|
||||
dicomImageLoaderInit({ maxWebWorkers: 1 });
|
||||
}
|
||||
|
||||
@@ -393,10 +402,51 @@ function App() {
|
||||
renderingEngineRef.current = renderingEngine;
|
||||
}
|
||||
|
||||
// 4. Setup tool group
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || setupTools();
|
||||
// --- In your setup function, use the correct tool group per viewport ---
|
||||
const { stackToolGroup, volumeToolGroup } = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID)
|
||||
? { stackToolGroup: ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID), volumeToolGroup: ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID) }
|
||||
: setupTools();
|
||||
|
||||
|
||||
const visibleVolumeViewports = viewportModes
|
||||
.slice(0, viewportGrid.rows * viewportGrid.cols)
|
||||
.filter(mode => mode === 'volume').length;
|
||||
|
||||
// After the viewport setup loop, outside the for-loop:
|
||||
if (
|
||||
crossReferenceEnabled &&
|
||||
visibleVolumeViewports >= 2
|
||||
) {
|
||||
if (!volumeToolGroup.hasTool(CrosshairsTool.toolName)) {
|
||||
volumeToolGroup.addTool(CrosshairsTool.toolName);
|
||||
}
|
||||
volumeToolGroup.setToolEnabled(CrosshairsTool.toolName);
|
||||
volumeToolGroup.setToolActive(CrosshairsTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }],
|
||||
});
|
||||
} else if (volumeToolGroup.hasTool(CrosshairsTool.toolName)) {
|
||||
volumeToolGroup.setToolDisabled(CrosshairsTool.toolName);
|
||||
}
|
||||
|
||||
// --- In your setup function, after tool group creation, enable/disable ReferenceLinesTool for each tool group ---
|
||||
if (referenceLinesEnabled) {
|
||||
if (!stackToolGroup.hasTool(ReferenceLinesTool.toolName)) {
|
||||
stackToolGroup.addTool(ReferenceLinesTool.toolName);
|
||||
}
|
||||
if (!volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) {
|
||||
volumeToolGroup.addTool(ReferenceLinesTool.toolName);
|
||||
}
|
||||
stackToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||
volumeToolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||
} else {
|
||||
if (stackToolGroup.hasTool(ReferenceLinesTool.toolName)) {
|
||||
stackToolGroup.setToolDisabled(ReferenceLinesTool.toolName);
|
||||
}
|
||||
if (volumeToolGroup.hasTool(ReferenceLinesTool.toolName)) {
|
||||
volumeToolGroup.setToolDisabled(ReferenceLinesTool.toolName);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Enable and configure each viewport
|
||||
for (let i = 0; i < MAX_VIEWPORTS; i++) {
|
||||
const viewportId = `CT_${i}`;
|
||||
@@ -408,6 +458,11 @@ function App() {
|
||||
? 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);
|
||||
@@ -425,25 +480,13 @@ function App() {
|
||||
// Add tool group only once
|
||||
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);
|
||||
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
|
||||
@@ -467,6 +510,17 @@ function App() {
|
||||
// Restore scroll, properties, etc.
|
||||
} else if (viewportModes[i] === 'volume') {
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Example: create and set a volume (you may need to adapt this for your data)
|
||||
const volumeId = 'myVolumeId';
|
||||
const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds });
|
||||
@@ -635,79 +689,51 @@ function App() {
|
||||
if (!loadedImageIds || loadedImageIds.length === 0) return;
|
||||
if (!orderBySliceLocation) return;
|
||||
|
||||
// Helper to get slice location from metadata
|
||||
const getSliceLocation = (imageId: string) => {
|
||||
const meta = metaData.get('imagePlaneModule', imageId);
|
||||
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
|
||||
let cancelled = false;
|
||||
|
||||
const checkAndSort = () => {
|
||||
// Check if all metadata is loaded
|
||||
const allLoaded = loadedImageIds.every(
|
||||
id => !!metaData.get("imagePlaneModule", id)
|
||||
);
|
||||
if (!allLoaded) {
|
||||
// Try again soon
|
||||
setTimeout(checkAndSort, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// All metadata loaded, sort by slice location
|
||||
const getSliceLocation = (imageId: string) => {
|
||||
const meta = metaData.get('imagePlaneModule', imageId);
|
||||
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
|
||||
};
|
||||
|
||||
const withSliceLoc = loadedImageIds.map(id => ({
|
||||
imageId: id,
|
||||
sliceLoc: getSliceLocation(id),
|
||||
}));
|
||||
|
||||
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);
|
||||
const isSame =
|
||||
loadedImageIds.length === sortedIds.length &&
|
||||
loadedImageIds.every((id, idx) => id === sortedIds[idx]);
|
||||
|
||||
if (!isSame && !cancelled) {
|
||||
setLoadedImageIds(sortedIds);
|
||||
}
|
||||
};
|
||||
|
||||
// Build array of { imageId, sliceLocation }
|
||||
const withSliceLoc = loadedImageIds.map(id => ({
|
||||
imageId: id,
|
||||
sliceLoc: getSliceLocation(id),
|
||||
}));
|
||||
checkAndSort();
|
||||
|
||||
// Sort by slice location, fallback to name if missing
|
||||
const sorted = [...withSliceLoc].sort((a, b) => {
|
||||
if (a.sliceLoc !== null && b.sliceLoc !== null) {
|
||||
return a.sliceLoc - b.sliceLoc;
|
||||
}
|
||||
return a.imageId.localeCompare(b.imageId);
|
||||
});
|
||||
|
||||
// Only update if the order actually changed
|
||||
const sortedIds = sorted.map(obj => obj.imageId);
|
||||
const isSame =
|
||||
loadedImageIds.length === sortedIds.length &&
|
||||
loadedImageIds.every((id, idx) => id === sortedIds[idx]);
|
||||
|
||||
if (!isSame) {
|
||||
setLoadedImageIds(sortedIds);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [orderBySliceLocation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadedImageIds || loadedImageIds.length === 0) return;
|
||||
// Only trigger if orderBySliceLocation is enabled
|
||||
if (!orderBySliceLocation) return;
|
||||
|
||||
// Check if all images have metadata loaded
|
||||
const allLoaded = loadedImageIds.every(
|
||||
id => !!metaData.get("imagePlaneModule", id)
|
||||
);
|
||||
if (!allLoaded) return;
|
||||
|
||||
// Helper to get slice location from metadata
|
||||
const getSliceLocation = (imageId: string) => {
|
||||
const meta = metaData.get('imagePlaneModule', imageId);
|
||||
return meta && meta.sliceLocation !== undefined ? Number(meta.sliceLocation) : null;
|
||||
};
|
||||
|
||||
// Build array of { imageId, sliceLocation }
|
||||
const withSliceLoc = loadedImageIds.map(id => ({
|
||||
imageId: id,
|
||||
sliceLoc: getSliceLocation(id),
|
||||
}));
|
||||
|
||||
// Sort by slice location, fallback to name if missing
|
||||
const sorted = [...withSliceLoc].sort((a, b) => {
|
||||
if (a.sliceLoc !== null && b.sliceLoc !== null) {
|
||||
return a.sliceLoc - b.sliceLoc;
|
||||
}
|
||||
return a.imageId.localeCompare(b.imageId);
|
||||
});
|
||||
|
||||
// Only update if the order actually changed
|
||||
const sortedIds = sorted.map(obj => obj.imageId);
|
||||
const isSame =
|
||||
loadedImageIds.length === sortedIds.length &&
|
||||
loadedImageIds.every((id, idx) => id === sortedIds[idx]);
|
||||
|
||||
if (!isSame) {
|
||||
setLoadedImageIds(sortedIds);
|
||||
}
|
||||
}, [orderBySliceLocation, loadedImageIds]);
|
||||
return () => { cancelled = true; };
|
||||
}, [loadedImageIds, orderBySliceLocation]);
|
||||
|
||||
|
||||
const viewports = [];
|
||||
@@ -1215,28 +1241,70 @@ function App() {
|
||||
>
|
||||
{crossReferenceEnabled ? "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>
|
||||
{/* 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: 12,
|
||||
top: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 100,
|
||||
// Always present for hover
|
||||
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={{
|
||||
opacity: 0,
|
||||
transition: "opacity 0.2s",
|
||||
pointerEvents: "auto",
|
||||
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",
|
||||
@@ -1246,13 +1314,18 @@ function App() {
|
||||
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 && (
|
||||
// ...your grid menu dropdown...
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -1268,6 +1341,7 @@ function App() {
|
||||
gridTemplateColumns: "repeat(3, 40px)",
|
||||
gap: "8px",
|
||||
zIndex: 200,
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{gridOptions.map(opt => (
|
||||
@@ -1297,6 +1371,7 @@ function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Viewport grid (inside main div) */}
|
||||
<div
|
||||
style={{
|
||||
@@ -1340,161 +1415,165 @@ function App() {
|
||||
</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)}
|
||||
>
|
||||
{
|
||||
settingsOpen && (
|
||||
<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",
|
||||
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={e => e.stopPropagation()}
|
||||
onClick={() => setSettingsOpen(false)}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
{TOOL_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 12,
|
||||
background: "transparent",
|
||||
background: "#222",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
fontSize: "20px",
|
||||
cursor: "pointer",
|
||||
borderRadius: "8px",
|
||||
minWidth: "320px",
|
||||
minHeight: "180px",
|
||||
padding: "24px",
|
||||
boxShadow: "0 4px 24px rgba(0,0,0,0.4)",
|
||||
position: "relative",
|
||||
}}
|
||||
onClick={() => setSettingsOpen(false)}
|
||||
aria-label="Close"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<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>
|
||||
))}
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
{TOOL_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
</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)}
|
||||
>
|
||||
)
|
||||
}
|
||||
{
|
||||
metaModalOpen && (
|
||||
<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",
|
||||
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={e => e.stopPropagation()}
|
||||
onClick={() => setMetaModalOpen(false)}
|
||||
>
|
||||
<div style={{ fontSize: "20px", marginBottom: "16px" }}>
|
||||
DICOM Metadata
|
||||
</div>
|
||||
{metaModalContent}
|
||||
<button
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 12,
|
||||
background: "transparent",
|
||||
background: "#222",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
fontSize: "20px",
|
||||
cursor: "pointer",
|
||||
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={() => setMetaModalOpen(false)}
|
||||
aria-label="Close"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
{/* ...rest of your UI... */}
|
||||
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1515,45 +1594,51 @@ function setupTools() {
|
||||
cornerstoneTools.addTool(PlanarFreehandContourSegmentationTool)
|
||||
cornerstoneTools.addTool(SculptorTool)
|
||||
cornerstoneTools.addTool(CrosshairsTool)
|
||||
cornerstoneTools.addTool(ReferenceLinesTool);
|
||||
|
||||
// Create tool group ONCE
|
||||
let toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||
if (!toolGroup) {
|
||||
toolGroup = ToolGroupManager.createToolGroup(toolGroupId)
|
||||
toolGroup.addTool(WindowLevelTool.toolName)
|
||||
toolGroup.addTool(PanTool.toolName)
|
||||
toolGroup.addTool(ZoomTool.toolName)
|
||||
toolGroup.addTool(StackScrollTool.toolName, { loop: false })
|
||||
toolGroup.addTool(PlanarRotateTool.toolName)
|
||||
toolGroup.addTool(LengthTool.toolName)
|
||||
toolGroup.addTool(ArrowAnnotateTool.toolName, {
|
||||
getTextCallback: () => '',
|
||||
})
|
||||
toolGroup.addTool(ProbeTool.toolName, {
|
||||
statsCalculator: () => null,
|
||||
})
|
||||
toolGroup.addTool(RectangleROITool.toolName, {
|
||||
calculateStats: false,
|
||||
getTextCallback: () => '',
|
||||
})
|
||||
toolGroup.addTool(EllipticalROITool.toolName, {
|
||||
getTextCallback: () => '',
|
||||
})
|
||||
toolGroup.addTool(PlanarFreehandROITool.toolName, {
|
||||
calculateStats: false,
|
||||
})
|
||||
toolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {})
|
||||
toolGroup.addTool(SculptorTool.toolName, {})
|
||||
|
||||
// Stack tool group (for stack viewports)
|
||||
let stackToolGroup = ToolGroupManager.getToolGroup(STACK_TOOL_GROUP_ID);
|
||||
if (!stackToolGroup) {
|
||||
stackToolGroup = ToolGroupManager.createToolGroup(STACK_TOOL_GROUP_ID);
|
||||
stackToolGroup.addTool(WindowLevelTool.toolName);
|
||||
stackToolGroup.addTool(PanTool.toolName);
|
||||
stackToolGroup.addTool(ZoomTool.toolName);
|
||||
stackToolGroup.addTool(StackScrollTool.toolName, { loop: false });
|
||||
stackToolGroup.addTool(PlanarRotateTool.toolName);
|
||||
stackToolGroup.addTool(LengthTool.toolName);
|
||||
stackToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' });
|
||||
stackToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
|
||||
stackToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
|
||||
stackToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
|
||||
stackToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
||||
stackToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
||||
stackToolGroup.addTool(SculptorTool.toolName, {});
|
||||
// StackScrollTool mouse wheel binding
|
||||
stackToolGroup.setToolActive(StackScrollTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Wheel }],
|
||||
});
|
||||
}
|
||||
|
||||
// The Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
|
||||
// and needs to be registered against the 'Wheel' binding.
|
||||
toolGroup.setToolActive(StackScrollTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Wheel, // Wheel Mouse
|
||||
},
|
||||
],
|
||||
})
|
||||
return toolGroup
|
||||
// Volume tool group (for volume/orthographic viewports)
|
||||
let volumeToolGroup = ToolGroupManager.getToolGroup(VOLUME_TOOL_GROUP_ID);
|
||||
if (!volumeToolGroup) {
|
||||
volumeToolGroup = ToolGroupManager.createToolGroup(VOLUME_TOOL_GROUP_ID);
|
||||
volumeToolGroup.addTool(StackScrollTool.toolName, { loop: false });
|
||||
volumeToolGroup.addTool(WindowLevelTool.toolName);
|
||||
volumeToolGroup.addTool(PanTool.toolName);
|
||||
volumeToolGroup.addTool(ZoomTool.toolName);
|
||||
volumeToolGroup.addTool(PlanarRotateTool.toolName);
|
||||
volumeToolGroup.addTool(LengthTool.toolName);
|
||||
volumeToolGroup.addTool(ArrowAnnotateTool.toolName, { getTextCallback: () => '' });
|
||||
volumeToolGroup.addTool(ProbeTool.toolName, { statsCalculator: () => null });
|
||||
volumeToolGroup.addTool(RectangleROITool.toolName, { calculateStats: false, getTextCallback: () => '' });
|
||||
volumeToolGroup.addTool(EllipticalROITool.toolName, { getTextCallback: () => '' });
|
||||
volumeToolGroup.addTool(PlanarFreehandROITool.toolName, { calculateStats: false });
|
||||
volumeToolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName, {});
|
||||
volumeToolGroup.addTool(SculptorTool.toolName, {});
|
||||
//volumeToolGroup.addTool(CrosshairsTool.toolName, {});
|
||||
}
|
||||
|
||||
return { stackToolGroup, volumeToolGroup };
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ html, body, #root {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.grid-menu-hover-container .grid-menu-btn {
|
||||
/* .grid-menu-hover-container .grid-menu-btn {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
@@ -18,4 +18,5 @@ html, body, #root {
|
||||
.grid-menu-btn:focus {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -63,13 +63,13 @@ import { readFile } from "dicom-parser"
|
||||
return imageIds
|
||||
} */
|
||||
|
||||
const NUM_IMAGES = 152 // set to however many images you have
|
||||
const NUM_IMAGES = 15 // set to however many images you have
|
||||
|
||||
export default async function createImageIds() {
|
||||
const imageIds = []
|
||||
for (let i = 1; i <= NUM_IMAGES; i++) {
|
||||
const fileName = `IMG${String(i)}.dcm`
|
||||
imageIds.push(`wadouri:/test_images/stack4/${fileName}`)
|
||||
imageIds.push(`wadouri:/test_images/stack2/${fileName}`)
|
||||
}
|
||||
return imageIds
|
||||
}
|
||||
Reference in New Issue
Block a user