lots of fixes
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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