start a c3d viewer
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import { useEffect, useRef, useCallback, useState } from "react"
|
||||
import createImageIdsAndCacheMetaData from "./lib/createImageIdsAndCacheMetaData"
|
||||
import {
|
||||
RenderingEngine,
|
||||
Enums,
|
||||
} from "@cornerstonejs/core"
|
||||
import { init as csRenderInit } from "@cornerstonejs/core"
|
||||
import { init as csToolsInit, addTool, ToolGroupManager, Enums as csToolsEnums, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, PlanarRotateTool } from "@cornerstonejs/tools"
|
||||
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import { StackScrollOutOfBoundsEvent } from 'core/src/types/EventTypes';
|
||||
|
||||
|
||||
const { MouseBindings } = csToolsEnums;
|
||||
|
||||
const { IMAGE_RENDERED } = Enums.Events;
|
||||
|
||||
const toolGroupId = 'STACK_TOOL_GROUP_ID';
|
||||
const leftClickTools = [
|
||||
WindowLevelTool.toolName,
|
||||
PlanarRotateTool.toolName,
|
||||
StackScrollTool.toolName,
|
||||
];
|
||||
const defaultLeftClickTool = leftClickTools[0];
|
||||
let currentLeftClickTool = leftClickTools[0];
|
||||
|
||||
// Common CT presets (add more as needed)
|
||||
const WINDOW_LEVEL_PRESETS = [
|
||||
{ name: "Soft Tissue", center: 40, width: 400 },
|
||||
{ name: "Lung", center: -600, width: 1500 },
|
||||
{ name: "Bone", center: 300, width: 1500 },
|
||||
{ name: "Brain", center: 40, width: 80 },
|
||||
{ name: "Abdomen", center: 60, width: 400 },
|
||||
]
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
const viewportRef = useRef<any>(null) // Store the viewport instance
|
||||
const running = useRef(false)
|
||||
|
||||
// State for overlay info
|
||||
const [imageInfo, setImageInfo] = useState({
|
||||
index: 0,
|
||||
total: 0,
|
||||
windowCenter: "",
|
||||
windowWidth: "",
|
||||
})
|
||||
|
||||
const updateOverlay = useCallback(() => {
|
||||
console.log("updateOverlay")
|
||||
const viewport = viewportRef.current
|
||||
if (!viewport) return
|
||||
console.log(viewport)
|
||||
|
||||
// For StackViewport in Cornerstone3D v3:
|
||||
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
||||
? viewport.getCurrentImageIdIndex()
|
||||
: 0
|
||||
const imageIds = typeof viewport.getImageIds === "function"
|
||||
? viewport.getImageIds()
|
||||
: []
|
||||
const total = imageIds.length
|
||||
|
||||
// Use getVOI() for current window center/width
|
||||
let windowCenter = ""
|
||||
let windowWidth = ""
|
||||
if (typeof viewport.getProperties === "function") {
|
||||
const props = viewport.getProperties()
|
||||
if (props.voiRange) {
|
||||
const { lower, upper } = props.voiRange
|
||||
windowCenter = ((upper + lower) / 2).toFixed(0)
|
||||
windowWidth = (upper - lower).toFixed(0)
|
||||
}
|
||||
}
|
||||
|
||||
setImageInfo({
|
||||
index: currentIndex + 1,
|
||||
total,
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
})
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const setup = async () => {
|
||||
if (running.current) {
|
||||
return
|
||||
}
|
||||
running.current = true
|
||||
|
||||
await csRenderInit()
|
||||
await csToolsInit()
|
||||
dicomImageLoaderInit({ maxWebWorkers: 1 })
|
||||
|
||||
// Add tools to Cornerstone3D
|
||||
cornerstoneTools.addTool(PanTool);
|
||||
cornerstoneTools.addTool(WindowLevelTool);
|
||||
cornerstoneTools.addTool(StackScrollTool);
|
||||
cornerstoneTools.addTool(ZoomTool);
|
||||
cornerstoneTools.addTool(PlanarRotateTool);
|
||||
|
||||
// Define a tool group, which defines how mouse events map to tool commands for
|
||||
// Any viewport using the group
|
||||
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
|
||||
|
||||
// Add tools to the tool group
|
||||
toolGroup.addTool(WindowLevelTool.toolName);
|
||||
toolGroup.addTool(PanTool.toolName);
|
||||
toolGroup.addTool(ZoomTool.toolName);
|
||||
toolGroup.addTool(StackScrollTool.toolName, { loop: false });
|
||||
toolGroup.addTool(PlanarRotateTool.toolName);
|
||||
|
||||
// Set the initial state of the tools, here all tools are active and bound to
|
||||
// Different mouse inputs
|
||||
toolGroup.setToolActive(defaultLeftClickTool, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Primary, // Left Click
|
||||
},
|
||||
],
|
||||
});
|
||||
toolGroup.setToolActive(PanTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Auxiliary, // Middle Click
|
||||
},
|
||||
],
|
||||
});
|
||||
toolGroup.setToolActive(ZoomTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Secondary, // Right Click
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 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
|
||||
},
|
||||
],
|
||||
});
|
||||
toolGroup.setToolActive(PlanarRotateTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Wheel, // Shift Wheel Mouse
|
||||
//modifierKey: KeyboardBindings.Shift,
|
||||
},
|
||||
{
|
||||
mouseButton: MouseBindings.Wheel_Primary, // Left Click+Wheel Mouse
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
|
||||
const imageIds = await createImageIdsAndCacheMetaData()
|
||||
console.log("imageIds", imageIds)
|
||||
|
||||
// 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)
|
||||
|
||||
cornerstoneTools.utilities.stackPrefetch.enable(viewport.element);
|
||||
|
||||
function test(e) {
|
||||
console.log("test", e)
|
||||
}
|
||||
|
||||
// Render the image
|
||||
viewport.render()
|
||||
viewport.element.addEventListener(Enums.Events.IMAGE_RENDERED, updateOverlay)
|
||||
///viewport.element.addEventListener(Enums.Events.STACK_NEW_IMAGE, updateOverlay)
|
||||
//viewport.element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateOverlay)
|
||||
//viewport.element.addEventListener(Enums.Events.PRESET_MODIFIED, updateOverlay)
|
||||
|
||||
// Initial overlay update
|
||||
updateOverlay()
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
// Create a stack viewport
|
||||
}, [elementRef, updateOverlay])
|
||||
|
||||
// Handler to reset the view (camera and window/level)
|
||||
const handleResetView = useCallback(() => {
|
||||
const viewport = viewportRef.current
|
||||
if (viewport && typeof viewport.resetCamera === "function") {
|
||||
viewport.resetCamera();
|
||||
viewport.resetProperties(); // Resets window/level and other properties
|
||||
viewport.render();
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Double-click handler (reuse the same logic)
|
||||
const handleDoubleClick = handleResetView
|
||||
|
||||
// Handler to apply a preset
|
||||
const handleApplyPreset = useCallback((center: number, width: number) => {
|
||||
const viewport = viewportRef.current
|
||||
if (viewport && typeof viewport.setProperties === "function") {
|
||||
const props = viewport.getProperties()
|
||||
viewport.setProperties({
|
||||
...props,
|
||||
voiRange: {
|
||||
lower: center - width / 2,
|
||||
upper: center + width / 2,
|
||||
},
|
||||
})
|
||||
viewport.render()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: "512px", height: "512px" }}>
|
||||
{/* ...existing Reset View button and viewport div... */}
|
||||
<button
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
zIndex: 10,
|
||||
padding: "6px 12px",
|
||||
background: "#222",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
opacity: 0.85,
|
||||
}}
|
||||
onClick={handleResetView}
|
||||
>
|
||||
Reset View
|
||||
</button>
|
||||
<div
|
||||
ref={elementRef}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
></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",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
Image: {imageInfo.index} / {imageInfo.total}
|
||||
</div>
|
||||
<div>
|
||||
WC: {imageInfo.windowCenter} WW: {imageInfo.windowWidth}
|
||||
</div>
|
||||
</div>
|
||||
{/* Preset menu at bottom right, collapsed by default, expands on hover */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(0,0,0,0.7)",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px",
|
||||
minWidth: "120px",
|
||||
overflow: "hidden",
|
||||
transition: "height 0.2s",
|
||||
height: "28px",
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
}}
|
||||
className="preset-menu"
|
||||
>
|
||||
<div style={{ padding: "6px 12px", fontWeight: "bold" }}>
|
||||
Presets
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(0,0,0,0.95)",
|
||||
borderRadius: "4px",
|
||||
padding: "6px 0 6px 0",
|
||||
display: "none",
|
||||
flexDirection: "column",
|
||||
zIndex: 11,
|
||||
}}
|
||||
className="preset-menu-content"
|
||||
>
|
||||
{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)}
|
||||
>
|
||||
{preset.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Inline CSS for hover effect */}
|
||||
<style>
|
||||
{`
|
||||
.preset-menu:hover .preset-menu-content {
|
||||
display: flex;
|
||||
}
|
||||
.preset-menu:hover {
|
||||
height: auto !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
Reference in New Issue
Block a user