work towards functional multiviewport
This commit is contained in:
+395
-279
@@ -53,6 +53,7 @@ const MOUSE_BUTTONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const elementRef = useRef<HTMLDivElement>(null)
|
const elementRef = useRef<HTMLDivElement>(null)
|
||||||
const viewportRef = useRef<any>(null) // Store the viewport instance
|
const viewportRef = useRef<any>(null) // Store the viewport instance
|
||||||
@@ -71,12 +72,16 @@ function App() {
|
|||||||
windowWidth: "",
|
windowWidth: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Store imageIds globally after loading so they can be reused
|
||||||
|
const [loadedImageIds, setLoadedImageIds] = useState<string[] | null>(null);
|
||||||
|
|
||||||
|
|
||||||
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
||||||
const [viewportMenuOpen, setViewportMenuOpen] = useState(false)
|
const [viewportMenuOpen, setViewportMenuOpen] = useState(false)
|
||||||
|
|
||||||
// Store refs for each viewport
|
// Store refs for each viewport
|
||||||
const viewportRefs = useRef<Array<HTMLDivElement | null>>([])
|
const viewportRefs = useRef<Array<HTMLDivElement | null>>([])
|
||||||
|
const renderingEnginesRef = useRef<Record<string, any>>({});
|
||||||
|
|
||||||
// Helper to generate grid options
|
// Helper to generate grid options
|
||||||
const gridOptions = []
|
const gridOptions = []
|
||||||
@@ -86,6 +91,26 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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: "",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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
|
// State for selected tool per mouse button
|
||||||
const [mouseToolBindings, setMouseToolBindings] = useState({
|
const [mouseToolBindings, setMouseToolBindings] = useState({
|
||||||
Primary: WindowLevelTool.toolName,
|
Primary: WindowLevelTool.toolName,
|
||||||
@@ -160,172 +185,161 @@ function App() {
|
|||||||
setMouseToolBindings(prev => ({ ...prev, [mouseButton]: toolName }))
|
setMouseToolBindings(prev => ({ ...prev, [mouseButton]: toolName }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateOverlay = useCallback(() => {
|
// Update overlay info for a specific viewport
|
||||||
const viewport = viewportRef.current
|
const updateOverlay = useCallback((viewportIdx: number, viewport: any) => {
|
||||||
if (!viewport) return
|
if (!viewport) return;
|
||||||
|
|
||||||
// For StackViewport in Cornerstone3D v3:
|
|
||||||
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
||||||
? viewport.getCurrentImageIdIndex()
|
? viewport.getCurrentImageIdIndex()
|
||||||
: 0
|
: 0;
|
||||||
const imageIds = typeof viewport.getImageIds === "function"
|
const imageIds = typeof viewport.getImageIds === "function"
|
||||||
? viewport.getImageIds()
|
? viewport.getImageIds()
|
||||||
: []
|
: [];
|
||||||
const total = imageIds.length
|
const total = imageIds.length;
|
||||||
|
|
||||||
// Use getVOI() for current window center/width
|
let windowCenter = "";
|
||||||
let windowCenter = ""
|
let windowWidth = "";
|
||||||
let windowWidth = ""
|
|
||||||
if (typeof viewport.getProperties === "function") {
|
if (typeof viewport.getProperties === "function") {
|
||||||
const props = viewport.getProperties()
|
const props = viewport.getProperties();
|
||||||
if (props.voiRange) {
|
if (props.voiRange) {
|
||||||
const { lower, upper } = props.voiRange
|
const { lower, upper } = props.voiRange;
|
||||||
windowCenter = ((upper + lower) / 2).toFixed(0)
|
windowCenter = ((upper + lower) / 2).toFixed(0);
|
||||||
windowWidth = (upper - lower).toFixed(0)
|
windowWidth = (upper - lower).toFixed(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setImageInfo({
|
setImageInfos(prev => {
|
||||||
index: currentIndex + 1,
|
const next = [...prev];
|
||||||
total,
|
next[viewportIdx] = {
|
||||||
windowCenter,
|
index: currentIndex + 1,
|
||||||
windowWidth,
|
total,
|
||||||
})
|
windowCenter,
|
||||||
}, [])
|
windowWidth,
|
||||||
|
};
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Store the properties of existing viewports before grid change
|
||||||
|
const prevViewportProps = useRef<Record<number, any>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
if (running.current) {
|
// 1. Capture state of existing viewports before grid change
|
||||||
return
|
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||||||
}
|
const renderingEngineId = `renderingEngine_${i}`;
|
||||||
running.current = true
|
const viewportId = `CT_${i}`;
|
||||||
|
try {
|
||||||
await csRenderInit()
|
const renderingEngine = renderingEnginesRef.current[renderingEngineId];
|
||||||
await csToolsInit()
|
if (renderingEngine) {
|
||||||
dicomImageLoaderInit({ maxWebWorkers: 1 })
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
if (viewport && typeof viewport.getProperties === "function") {
|
||||||
// Add tools to Cornerstone3D
|
prevViewportProps.current[i] = viewport.getProperties();
|
||||||
cornerstoneTools.addTool(PanTool)
|
}
|
||||||
cornerstoneTools.addTool(WindowLevelTool)
|
}
|
||||||
cornerstoneTools.addTool(StackScrollTool)
|
} catch (e) {
|
||||||
cornerstoneTools.addTool(ZoomTool)
|
// Ignore if not found
|
||||||
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)
|
|
||||||
|
|
||||||
// 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, {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const imageIds = await createImageIdsAndCacheMetaData()
|
|
||||||
|
|
||||||
// Instantiate a rendering engine
|
|
||||||
const renderingEngineId = "myRenderingEngine"
|
|
||||||
const renderingEngine = new RenderingEngine(renderingEngineId)
|
|
||||||
const viewportId = "CT"
|
|
||||||
|
|
||||||
|
|
||||||
const viewportInput = {
|
|
||||||
viewportId,
|
|
||||||
type: Enums.ViewportType.STACK,
|
|
||||||
element: elementRef.current,
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingEngine.enableElement(viewportInput)
|
|
||||||
|
|
||||||
// Set the tool group on the viewport
|
|
||||||
toolGroup.addViewport(viewportId, renderingEngineId);
|
|
||||||
|
|
||||||
// Get the stack viewport that was created
|
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
|
||||||
viewportRef.current = viewport; // Save for later use
|
|
||||||
|
|
||||||
// Create a stack volume
|
|
||||||
viewport.setStack(imageIds)
|
|
||||||
|
|
||||||
const segmentationId = 'my-segmentation-id';
|
|
||||||
|
|
||||||
// Create a segmentation representation for the stack
|
|
||||||
segmentation.addSegmentations([{
|
|
||||||
segmentationId,
|
|
||||||
representation: {
|
|
||||||
type: csEnums.SegmentationRepresentations.Contour
|
|
||||||
}
|
}
|
||||||
}]);
|
}
|
||||||
//segmentation.activeSegmentation.setActiveSegmentationRepresentations(segmentationId);
|
|
||||||
// Create a segmentation representation associated to the viewportId
|
|
||||||
await segmentation.addSegmentationRepresentations(viewportId, [
|
|
||||||
{
|
|
||||||
segmentationId,
|
|
||||||
type: csToolsEnums.SegmentationRepresentations.Contour,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 1);
|
if (running.current) {
|
||||||
cornerstoneTools.utilities.stackPrefetch.enable(viewport.element);
|
if (loadedImageIds) {
|
||||||
|
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||||||
|
const renderingEngineId = `renderingEngine_${i}`;
|
||||||
|
const viewportId = `CT_${i}`;
|
||||||
|
const element = viewportRefs.current[i];
|
||||||
|
if (!element) continue;
|
||||||
|
|
||||||
// Render the image
|
const renderingEngine = new RenderingEngine(renderingEngineId);
|
||||||
viewport.render()
|
renderingEnginesRef.current[renderingEngineId] = renderingEngine;
|
||||||
viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, updateOverlay)
|
const viewportInput = {
|
||||||
///viewport.element.addEventListener(Enums.Events.STACK_NEW_IMAGE, updateOverlay)
|
viewportId,
|
||||||
//viewport.element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateOverlay)
|
type: Enums.ViewportType.STACK,
|
||||||
//viewport.element.addEventListener(Enums.Events.PRESET_MODIFIED, updateOverlay)
|
element,
|
||||||
|
};
|
||||||
|
renderingEngine.enableElement(viewportInput);
|
||||||
|
|
||||||
// Initial overlay update
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||||
updateOverlay()
|
toolGroup?.addViewport(viewportId, renderingEngineId);
|
||||||
|
|
||||||
// Apply mouse tool bindings after setup
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
applyMouseToolBindings()
|
viewport.setStack(loadedImageIds);
|
||||||
}
|
|
||||||
|
|
||||||
setup()
|
/* // Restore previous state for existing viewports only
|
||||||
|
if (prevViewportProps.current[i] && typeof viewport.setProperties === "function") {
|
||||||
|
viewport.setProperties(prevViewportProps.current[i]);
|
||||||
|
} */
|
||||||
|
|
||||||
// Create a stack viewport
|
cornerstoneTools.utilities.stackPrefetch.enable(viewport.element);
|
||||||
}, [elementRef, updateOverlay])
|
console.log(i, viewport)
|
||||||
|
viewport.resetCameraForResize();
|
||||||
|
viewport.render();
|
||||||
|
|
||||||
|
// Attach overlay update for each viewport
|
||||||
|
viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, () => updateOverlay(i, viewport));
|
||||||
|
// Initial overlay update
|
||||||
|
updateOverlay(i, viewport);
|
||||||
|
}
|
||||||
|
applyMouseToolBindings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
running.current = true;
|
||||||
|
|
||||||
|
await csRenderInit();
|
||||||
|
await csToolsInit();
|
||||||
|
dicomImageLoaderInit({ maxWebWorkers: 1 });
|
||||||
|
|
||||||
|
const toolGroup = setupTools();
|
||||||
|
const imageIds = await createImageIdsAndCacheMetaData();
|
||||||
|
setLoadedImageIds(imageIds);
|
||||||
|
|
||||||
|
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||||||
|
const renderingEngineId = `renderingEngine_${i}`;
|
||||||
|
const viewportId = `CT_${i}`;
|
||||||
|
const element = viewportRefs.current[i];
|
||||||
|
if (!element) continue;
|
||||||
|
|
||||||
|
const renderingEngine = new RenderingEngine(renderingEngineId);
|
||||||
|
renderingEnginesRef.current[renderingEngineId] = renderingEngine;
|
||||||
|
const viewportInput = {
|
||||||
|
viewportId,
|
||||||
|
type: Enums.ViewportType.STACK,
|
||||||
|
element,
|
||||||
|
};
|
||||||
|
renderingEngine.enableElement(viewportInput);
|
||||||
|
|
||||||
|
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
viewport.setStack(imageIds);
|
||||||
|
|
||||||
|
// Restore previous state for existing viewports only
|
||||||
|
if (prevViewportProps.current[i] && typeof viewport.setProperties === "function") {
|
||||||
|
viewport.setProperties(prevViewportProps.current[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstoneTools.utilities.stackPrefetch.enable(viewport.element);
|
||||||
|
viewport.resize();
|
||||||
|
viewport.render();
|
||||||
|
|
||||||
|
console.log(viewport)
|
||||||
|
|
||||||
|
// Attach overlay update for each viewport
|
||||||
|
viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, () => updateOverlay(i, viewport));
|
||||||
|
// Initial overlay update
|
||||||
|
updateOverlay(i, viewport);
|
||||||
|
}
|
||||||
|
applyMouseToolBindings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delay setup by 100ms (adjust as needed)
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setup();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [elementRef, updateOverlay, viewportGrid, loadedImageIds]);
|
||||||
|
|
||||||
// Handler to reset the view (camera and window/level)
|
// Handler to reset the view (camera and window/level)
|
||||||
const handleResetView = useCallback(() => {
|
const handleResetView = useCallback(() => {
|
||||||
@@ -340,21 +354,42 @@ function App() {
|
|||||||
// Double-click handler (reuse the same logic)
|
// Double-click handler (reuse the same logic)
|
||||||
const handleDoubleClick = handleResetView
|
const handleDoubleClick = handleResetView
|
||||||
|
|
||||||
// Handler to apply a preset
|
// Handler to apply a preset for a specific viewport
|
||||||
const handleApplyPreset = useCallback((center: number, width: number) => {
|
const handleApplyPreset = useCallback((viewportIdx: number, center: number, width: number) => {
|
||||||
const viewport = viewportRef.current
|
console.log("Applying preset:", center, width);
|
||||||
if (viewport && typeof viewport.setProperties === "function") {
|
const renderingEngineId = `renderingEngine_${viewportIdx}`;
|
||||||
const props = viewport.getProperties()
|
const viewportId = `CT_${viewportIdx}`;
|
||||||
viewport.setProperties({
|
try {
|
||||||
...props,
|
const renderingEngine = renderingEnginesRef.current[renderingEngineId];
|
||||||
voiRange: {
|
console.log("Rendering engine:", renderingEngine);
|
||||||
lower: center - width / 2,
|
if (renderingEngine) {
|
||||||
upper: center + width / 2,
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
},
|
if (viewport && typeof viewport.setProperties === "function") {
|
||||||
})
|
viewport.setProperties({
|
||||||
viewport.render()
|
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
|
// Re-apply bindings when tool sets or ctrl changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -374,9 +409,18 @@ function App() {
|
|||||||
return () => document.removeEventListener("mousedown", handleClick);
|
return () => document.removeEventListener("mousedown", handleClick);
|
||||||
}, [presetsOpen]);
|
}, [presetsOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleContextMenu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
document.addEventListener("contextmenu", handleContextMenu);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("contextmenu", handleContextMenu);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Render the grid of viewports
|
// Render the grid of viewports
|
||||||
const viewports = []
|
const viewports = [];
|
||||||
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
for (let i = 0; i < viewportGrid.rows * viewportGrid.cols; i++) {
|
||||||
viewports.push(
|
viewports.push(
|
||||||
<div
|
<div
|
||||||
@@ -389,15 +433,132 @@ function App() {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* You can render overlays or controls per-viewport here */}
|
{/* 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Image: {imageInfos[i]?.index} / {imageInfos[i]?.total}
|
||||||
|
</div>
|
||||||
|
<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={() => {
|
||||||
|
console.log("Preset clicked for viewport", i, preset.name);
|
||||||
|
handleApplyPreset(i, preset.center, preset.width);
|
||||||
|
setPresetsOpenArr(prev => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[i] = false;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preset.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative", width: "512px", height: "512px" }}>
|
<div style={{ position: "relative", width: "100vw", height: "100vh", background: "#111" }}>
|
||||||
|
{/* Reset View Button */}
|
||||||
<button
|
<button
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -411,22 +572,13 @@ function App() {
|
|||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
userSelect: "none", // <-- Prevents text selection
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
onClick={handleResetView}
|
onClick={handleResetView}
|
||||||
>
|
>
|
||||||
Reset View
|
Reset View
|
||||||
</button>
|
</button>
|
||||||
<div
|
|
||||||
ref={elementRef}
|
|
||||||
onDoubleClick={handleDoubleClick}
|
|
||||||
onContextMenu={e => e.preventDefault()} // <-- Add this line
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "#000",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
{/* Viewport grid menu at top center */}
|
{/* Viewport grid menu at top center */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -497,119 +649,25 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Viewport grid */}
|
{/* Viewport grid (inside main div) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
|
gridTemplateRows: `repeat(${viewportGrid.rows}, 1fr)`,
|
||||||
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
|
gridTemplateColumns: `repeat(${viewportGrid.cols}, 1fr)`,
|
||||||
width: "100vw",
|
|
||||||
height: "100vh",
|
|
||||||
gap: "2px",
|
gap: "2px",
|
||||||
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{viewports}
|
{viewports}
|
||||||
</div>
|
</div>
|
||||||
{/* Overlay at bottom left */}
|
|
||||||
<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", // <-- Prevents text selection
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
Image: {imageInfo.index} / {imageInfo.total}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
WC: {imageInfo.windowCenter} WW: {imageInfo.windowWidth}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Preset menu at bottom right, expands on click */}
|
|
||||||
<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: presetsOpen ? "0 2px 8px rgba(0,0,0,0.3)" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "6px 12px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
onClick={() => setPresetsOpen((open) => !open)}
|
|
||||||
>
|
|
||||||
Presets
|
|
||||||
<span style={{ float: "right", fontWeight: "normal" }}>
|
|
||||||
{presetsOpen ? "▲" : "▼"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{presetsOpen && (
|
|
||||||
<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(preset.center, preset.width);
|
|
||||||
setPresetsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{preset.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Settings button at top right */}
|
{/* Settings button at top right */}
|
||||||
<button
|
<button
|
||||||
style={{
|
style={{
|
||||||
@@ -736,3 +794,61 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
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)
|
||||||
|
|
||||||
|
// 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, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user