start a c3d viewer

This commit is contained in:
Ross
2025-05-19 13:39:53 +01:00
commit 63990c49ee
61 changed files with 10847 additions and 0 deletions
+373
View File
@@ -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} &nbsp; 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