Compare commits
14 Commits
feat/test-
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
e5bf5685cf | ||
|
dff24401bf | ||
|
d09d852207 | ||
|
ba516af2f7 | ||
|
a0f4ca2f3a | ||
|
cc783768db | ||
|
7291840ed8 | ||
|
f2f7382fec | ||
|
98b5b354d9 | ||
|
9917b77077 | ||
|
5814637d7a | ||
|
21914f3077 | ||
|
c52b3f1e00 | ||
|
039325b768 |
|
@ -23,6 +23,8 @@ pnpm config set virtual-store-dir-max-length 70
|
|||
|
||||
- 不同结构的窗宽窗位快速选择,需要考虑不同身体结构的dicom image
|
||||
|
||||
- 炸显存问题,electron关闭硬件加速,还是无法接解决1000+外周的mpr
|
||||
|
||||
## 窗宽创维相关的一些小问题
|
||||
|
||||
- 心脏软组织窗
|
||||
|
|
|
@ -8,11 +8,12 @@ export const registerDicomHandler = () => {
|
|||
const dia = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
if (dia.canceled) return null;
|
||||
const dcmPaths = await filterDicoms(dia.filePaths[0]);
|
||||
uploadFilesInBatches({
|
||||
await uploadFilesInBatches({
|
||||
filePaths: dcmPaths,
|
||||
batchSize: 6,
|
||||
feedback: (d) => event.reply("dicom:upload:detail", d),
|
||||
});
|
||||
event.reply("dicom:upload:finished"); // 重新刷新病人列表
|
||||
});
|
||||
|
||||
ipcMain.handle("dicom:select", async () => {
|
||||
|
|
|
@ -38,7 +38,11 @@ const themeTitleBarStyles = {
|
|||
|
||||
export const platform = process.platform === "darwin" ? "macos" : "windows";
|
||||
|
||||
app.commandLine.appendSwitch('disable-web-security');
|
||||
app.commandLine.appendSwitch("disable-web-security");
|
||||
app.commandLine.appendSwitch("ignore-gpu-blocklist");
|
||||
app.commandLine.appendSwitch("use-angle", "gl");
|
||||
|
||||
// app.disableHardwareAcceleration()
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
|
@ -51,7 +55,7 @@ function createWindow() {
|
|||
titleBarOverlay: { height: 36, ...themeTitleBarStyles[theme] }, // 渲染进程发消息动态改变这个
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.mjs"),
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export const Datasource = () => {
|
|||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPatients = () => {
|
||||
window.ipcRenderer
|
||||
.invoke("dicom:select")
|
||||
.then((patients: PatientInfo[]) => {
|
||||
|
@ -31,6 +31,17 @@ export const Datasource = () => {
|
|||
rawPatientsRef.current = patients;
|
||||
setPatients(patients);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPatients();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.ipcRenderer.on("dicom:upload:finished", fetchPatients);
|
||||
return () => {
|
||||
window.ipcRenderer.off("dicom:upload:finished", fetchPatients);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePatientSearch = (filterValue: string) => {
|
||||
|
|
3
apps/desktop/src/pages/Viewer/ModelViewer/index.tsx
Normal file
3
apps/desktop/src/pages/Viewer/ModelViewer/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const Model3DViewer = () => {
|
||||
return <div>3d model</div>;
|
||||
};
|
|
@ -9,9 +9,12 @@ export const initCornerstoneDICOMImageLoader = () => {
|
|||
cornerstone.setUseSharedArrayBuffer(false);
|
||||
cornerstone.setConfiguration({
|
||||
detectGPUConfig: {
|
||||
// benchmarksURL: "http://localhost:9000",
|
||||
benchmarksURL: "",
|
||||
},
|
||||
rendering: {
|
||||
...cornerstone.getConfiguration().rendering,
|
||||
// useNorm16Texture: true,
|
||||
},
|
||||
rendering: cornerstone.getConfiguration().rendering,
|
||||
isMobile: false,
|
||||
enableCacheOptimization: false,
|
||||
});
|
||||
|
@ -22,6 +25,7 @@ export const initCornerstoneDICOMImageLoader = () => {
|
|||
useWebWorkers: true,
|
||||
decodeConfig: {
|
||||
convertFloatPixelDataToInt: false,
|
||||
// use16BitDataType: false,
|
||||
use16BitDataType: preferSizeOverAccuracy || useNorm16Texture,
|
||||
},
|
||||
});
|
||||
|
@ -37,6 +41,7 @@ export const initCornerstoneDICOMImageLoader = () => {
|
|||
startWebWorkersOnDemand: false,
|
||||
taskConfiguration: {
|
||||
decodeTask: {
|
||||
useNorm16Texture: true,
|
||||
initializeCodecsOnStartup: false,
|
||||
strict: false,
|
||||
},
|
||||
|
|
|
@ -9,7 +9,6 @@ export interface CtTransferFunction {
|
|||
|
||||
export default function setCtTransferFunctionForVolumeActor(p: CtTransferFunction) {
|
||||
const { volumeActor, defaultWindowCenter, defaultWindowWidth } = p
|
||||
console.log(defaultWindowCenter, defaultWindowWidth)
|
||||
const lower = defaultWindowCenter - defaultWindowWidth / 2.0;
|
||||
const upper = defaultWindowCenter + defaultWindowWidth / 2.0;
|
||||
ctVoiRange = { lower, upper };
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import * as cornerstoneTools from "@cornerstonejs/tools";
|
||||
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
||||
import setCtTransferFunctionForVolumeActor from "./CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor";
|
||||
import {
|
||||
RenderingEngine,
|
||||
setVolumesForViewports,
|
||||
Enums as CoreEnums,
|
||||
} from "@cornerstonejs/core";
|
||||
import {
|
||||
viewportColors,
|
||||
viewportReferenceLineControllable,
|
||||
viewportReferenceLineDraggableRotatable,
|
||||
viewportReferenceLineSlabThicknessControlsOn,
|
||||
viewportId1,
|
||||
viewportId2,
|
||||
viewportId3,
|
||||
ViewportId,
|
||||
} from "./Crosshair.config";
|
||||
|
||||
const {
|
||||
ToolGroupManager,
|
||||
CrosshairsTool,
|
||||
StackScrollMouseWheelTool,
|
||||
WindowLevelTool,
|
||||
ZoomTool,
|
||||
Enums: csToolsEnums,
|
||||
} = cornerstoneTools;
|
||||
|
||||
const { MouseBindings } = csToolsEnums;
|
||||
|
||||
const { ViewportType } = CoreEnums;
|
||||
|
||||
function getReferenceLineColor(vpId: ViewportId) {
|
||||
return viewportColors[vpId];
|
||||
}
|
||||
|
||||
function getReferenceLineControllable(vpId: ViewportId) {
|
||||
const index = viewportReferenceLineControllable.indexOf(vpId);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
function getReferenceLineDraggableRotatable(vpId: ViewportId) {
|
||||
const index = viewportReferenceLineDraggableRotatable.indexOf(vpId);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
function getReferenceLineSlabThicknessControlsOn(vpId: ViewportId) {
|
||||
const index = viewportReferenceLineSlabThicknessControlsOn.indexOf(vpId);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
interface CrosshairMprProps {
|
||||
wwwl: {
|
||||
windowCenter: number;
|
||||
windowWidth: number;
|
||||
};
|
||||
volumeId: string;
|
||||
}
|
||||
|
||||
export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewportRef_AXIAL = useRef<HTMLDivElement | null>(null);
|
||||
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
|
||||
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
|
||||
const renderingEngine = useRef<RenderingEngine>();
|
||||
const toolGroupRef = useRef<cornerstoneTools.Types.IToolGroup | undefined>(
|
||||
undefined
|
||||
);
|
||||
const ts = "-" + Date.now();
|
||||
const toolGroupId = "mprToolGroup" + ts;
|
||||
const renderingEngineId = "mprRenderingEngine" + ts;
|
||||
|
||||
useEffect(() => {
|
||||
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
||||
cornerstoneTools.addTool(CrosshairsTool);
|
||||
cornerstoneTools.addTool(WindowLevelTool);
|
||||
cornerstoneTools.addTool(ZoomTool);
|
||||
|
||||
toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId);
|
||||
|
||||
const run = async () => {
|
||||
if (
|
||||
!viewportRef_AXIAL.current ||
|
||||
!viewportRef_SAGITTAL.current ||
|
||||
!viewportRef_CORONAL.current
|
||||
)
|
||||
return;
|
||||
|
||||
renderingEngine.current = new RenderingEngine(renderingEngineId);
|
||||
|
||||
// Create the viewports
|
||||
const viewportInputArray: PublicViewportInput[] = [
|
||||
{
|
||||
viewportId: viewportId1,
|
||||
type: ViewportType.ORTHOGRAPHIC,
|
||||
element: viewportRef_AXIAL.current,
|
||||
defaultOptions: {
|
||||
orientation: CoreEnums.OrientationAxis.AXIAL,
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportId: viewportId2,
|
||||
type: ViewportType.ORTHOGRAPHIC,
|
||||
element: viewportRef_SAGITTAL.current,
|
||||
defaultOptions: {
|
||||
orientation: CoreEnums.OrientationAxis.SAGITTAL,
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportId: viewportId3,
|
||||
type: ViewportType.ORTHOGRAPHIC,
|
||||
element: viewportRef_CORONAL.current,
|
||||
defaultOptions: {
|
||||
orientation: CoreEnums.OrientationAxis.CORONAL,
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
renderingEngine.current.setViewports(viewportInputArray);
|
||||
|
||||
// Set volumes on the viewports
|
||||
await setVolumesForViewports(
|
||||
renderingEngine.current,
|
||||
[
|
||||
{
|
||||
volumeId: props.volumeId,
|
||||
callback: ({ volumeActor }) =>
|
||||
setCtTransferFunctionForVolumeActor({
|
||||
volumeActor,
|
||||
defaultWindowCenter: props.wwwl.windowCenter,
|
||||
defaultWindowWidth: props.wwwl.windowWidth,
|
||||
}),
|
||||
},
|
||||
],
|
||||
[viewportId1, viewportId2, viewportId3]
|
||||
);
|
||||
|
||||
if (toolGroupRef.current) {
|
||||
// For the crosshairs to operate, the viewports must currently be
|
||||
// added ahead of setting the tool active. This will be improved in the future.
|
||||
toolGroupRef.current.addViewport(viewportId1, renderingEngineId);
|
||||
toolGroupRef.current.addViewport(viewportId2, renderingEngineId);
|
||||
toolGroupRef.current.addViewport(viewportId3, renderingEngineId);
|
||||
|
||||
/**
|
||||
* zoom影像
|
||||
*/
|
||||
toolGroupRef.current.addTool(ZoomTool.toolName);
|
||||
toolGroupRef.current.setToolActive(ZoomTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Secondary, // 鼠标中键
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Manipulation Tools
|
||||
toolGroupRef.current.addTool(StackScrollMouseWheelTool.toolName);
|
||||
// Add Crosshairs tool and configure it to link the three viewports
|
||||
// These viewports could use different tool groups. See the PET-CT example
|
||||
// for a more complicated used case.
|
||||
|
||||
toolGroupRef.current.addTool(CrosshairsTool.toolName, {
|
||||
getReferenceLineColor,
|
||||
getReferenceLineControllable,
|
||||
getReferenceLineDraggableRotatable,
|
||||
getReferenceLineSlabThicknessControlsOn,
|
||||
});
|
||||
|
||||
toolGroupRef.current.setToolActive(CrosshairsTool.toolName, {
|
||||
bindings: [{ mouseButton: 1 }],
|
||||
});
|
||||
// As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
|
||||
// hook instead of mouse buttons, it does not need to assign any mouse button.
|
||||
toolGroupRef.current.setToolActive(StackScrollMouseWheelTool.toolName);
|
||||
|
||||
toolGroupRef.current.addTool(WindowLevelTool.toolName);
|
||||
toolGroupRef.current.setToolActive(WindowLevelTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Auxiliary,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
renderingEngine.current.renderViewports([
|
||||
viewportId1,
|
||||
viewportId2,
|
||||
viewportId3,
|
||||
]);
|
||||
};
|
||||
|
||||
run();
|
||||
|
||||
return () => {
|
||||
// 禁用视口
|
||||
renderingEngine.current?.disableElement(viewportId1);
|
||||
renderingEngine.current?.disableElement(viewportId2);
|
||||
renderingEngine.current?.disableElement(viewportId3);
|
||||
|
||||
// 销毁渲染引擎
|
||||
renderingEngine.current?.destroy();
|
||||
|
||||
// 从 ToolGroupManager 中移除工具组
|
||||
ToolGroupManager.destroyToolGroup(toolGroupId);
|
||||
|
||||
// 移出工具注册
|
||||
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
|
||||
cornerstoneTools.removeTool(CrosshairsTool);
|
||||
cornerstoneTools.removeTool(WindowLevelTool);
|
||||
cornerstoneTools.removeTool(ZoomTool);
|
||||
};
|
||||
}, [props, renderingEngineId, toolGroupId]);
|
||||
|
||||
/**
|
||||
* mpr resize
|
||||
*/
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
let resizeTimeout: NodeJS.Timeout | null = null;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => renderingEngine.current?.resize(), 100);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
return () => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeObserver.unobserve(container);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full flex flex-col">
|
||||
<div className="w-full h-1/3" ref={viewportRef_AXIAL} />
|
||||
<div className="w-full h-1/3" ref={viewportRef_SAGITTAL} />
|
||||
<div className="w-full h-1/3" ref={viewportRef_CORONAL} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -5,6 +5,11 @@ export type ViewportId = "CT_AXIAL" | "CT_SAGITTAL" | "CT_CORONAL";
|
|||
export const viewportId1: ViewportId = "CT_AXIAL";
|
||||
export const viewportId2: ViewportId = "CT_SAGITTAL";
|
||||
export const viewportId3: ViewportId = "CT_CORONAL";
|
||||
export const viewportIds: ViewportId[] = [
|
||||
viewportId1,
|
||||
viewportId2,
|
||||
viewportId3,
|
||||
];
|
||||
|
||||
export const viewportColors = {
|
||||
[viewportId1]: "rgb(200, 0, 0)",
|
||||
|
@ -34,3 +39,24 @@ export const viewportReferenceLineSlabThicknessControlsOn = [
|
|||
export const volumeName = "CT_VOLUME_ID"; // Id of the volume less loader prefix
|
||||
export const volumeLoaderScheme = "cornerstoneStreamingImageVolume"; // Loader id which defines which volume loader to use
|
||||
export const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id
|
||||
export const toolGroupMprId = "toolMpr";
|
||||
export const toolGroupStackId = "toolStack";
|
||||
|
||||
export function getReferenceLineColor(vpId: ViewportId) {
|
||||
return viewportColors[vpId];
|
||||
}
|
||||
|
||||
export function getReferenceLineControllable(vpId: ViewportId) {
|
||||
const index = viewportReferenceLineControllable.indexOf(vpId);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
export function getReferenceLineDraggableRotatable(vpId: ViewportId) {
|
||||
const index = viewportReferenceLineDraggableRotatable.indexOf(vpId);
|
||||
return index !== -1;
|
||||
}
|
||||
|
||||
export function getReferenceLineSlabThicknessControlsOn(vpId: ViewportId) {
|
||||
const index = viewportReferenceLineSlabThicknessControlsOn.indexOf(vpId);
|
||||
return index !== -1;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export const stackViewportId = "stackNo1";
|
||||
export const toolGroupStackId = "toolStackNo1";
|
|
@ -1,79 +0,0 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
||||
import { Enums as CoreEnums, RenderingEngine } from "@cornerstonejs/core";
|
||||
import { IStackViewport } from "@cornerstonejs/core/dist/types/types";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
export interface StackViewerProps {
|
||||
imageIds: string[];
|
||||
}
|
||||
|
||||
export const StackViewer = (props: StackViewerProps) => {
|
||||
const viewportStackRef = useRef<HTMLDivElement | null>(null);
|
||||
const renderingEngineRef = useRef<RenderingEngine>();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewportId = "stackViewport";
|
||||
const renderingEngineId = "stackRenderingEngine";
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewportStackRef.current) return;
|
||||
renderingEngineRef.current = new RenderingEngine(renderingEngineId);
|
||||
const viewportInput: PublicViewportInput = {
|
||||
viewportId,
|
||||
type: CoreEnums.ViewportType.STACK, // 用于 Stack 视图
|
||||
element: viewportStackRef.current,
|
||||
defaultOptions: {
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
};
|
||||
renderingEngineRef.current.enableElement(viewportInput);
|
||||
const viewport = renderingEngineRef.current.getViewport(
|
||||
viewportId
|
||||
) as IStackViewport;
|
||||
|
||||
viewport.setStack(props.imageIds);
|
||||
|
||||
return () => {
|
||||
renderingEngineRef.current?.disableElement(viewportId);
|
||||
renderingEngineRef.current?.destroy();
|
||||
};
|
||||
}, [props.imageIds, renderingEngineId, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
let resizeTimeout: NodeJS.Timeout | null = null;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(
|
||||
() => renderingEngineRef.current?.resize(),
|
||||
100
|
||||
);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
return () => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeObserver.unobserve(container);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onChangeIndex = (value: number[]) => {
|
||||
if (renderingEngineRef.current) {
|
||||
const viewport = renderingEngineRef.current.getViewport(viewportId);
|
||||
(viewport as IStackViewport).setImageIdIndex(value[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full flex flex-col pb-4">
|
||||
<div className="w-full h-full" ref={viewportStackRef} />
|
||||
<Slider
|
||||
defaultValue={[0]}
|
||||
max={props.imageIds.length - 1}
|
||||
step={1}
|
||||
onValueChange={onChangeIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
11
apps/desktop/src/pages/Viewer/index.css
Normal file
11
apps/desktop/src/pages/Viewer/index.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.stack-slider {
|
||||
>span:first-child{
|
||||
height: 8px;
|
||||
border-radius: 0;
|
||||
}
|
||||
[role="slider"] {
|
||||
width: 16px;
|
||||
height: 8px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
|
@ -1,62 +1,263 @@
|
|||
import { useLocation } from "react-router-dom";
|
||||
import { initCornerstone } from "./MprViewer/CornerstoneDicomLoader/init";
|
||||
import { CrosshairMpr } from "./MprViewer/Crosshair";
|
||||
import * as cornerstoneTools from "@cornerstonejs/tools";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { StackViewer } from "./StackViewer";
|
||||
import { createImageIdsAndCacheMetaData } from "./MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
|
||||
import { volumeLoader } from "@cornerstonejs/core";
|
||||
import {
|
||||
RenderingEngine,
|
||||
setVolumesForViewports,
|
||||
volumeLoader,
|
||||
Enums as CoreEnums,
|
||||
cache,
|
||||
} from "@cornerstonejs/core";
|
||||
|
||||
import {
|
||||
IStackViewport,
|
||||
PublicViewportInput,
|
||||
} from "@cornerstonejs/core/dist/types/types";
|
||||
import {
|
||||
getReferenceLineColor,
|
||||
getReferenceLineControllable,
|
||||
getReferenceLineDraggableRotatable,
|
||||
getReferenceLineSlabThicknessControlsOn,
|
||||
toolGroupMprId,
|
||||
toolGroupStackId,
|
||||
viewportIds,
|
||||
} from "./MprViewer/index.config";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { stackViewportId } from "./StackViewer/index.config";
|
||||
import { ToolBarMenu } from "./ToolBarMenu";
|
||||
import { Model3DViewer } from "./ModelViewer";
|
||||
import useMultiResizeObserver from "./useMultiResizeObserver";
|
||||
import "./index.css";
|
||||
|
||||
const {
|
||||
ToolGroupManager,
|
||||
CrosshairsTool,
|
||||
StackScrollMouseWheelTool,
|
||||
WindowLevelTool,
|
||||
ZoomTool,
|
||||
Enums: csToolsEnums,
|
||||
} = cornerstoneTools;
|
||||
|
||||
const { MouseBindings } = csToolsEnums;
|
||||
|
||||
const wadoRsRoot = "http://localhost:8042/dicom-web";
|
||||
const renderingEngineId = "renderEngineNo1";
|
||||
|
||||
const { ViewportType, OrientationAxis } = CoreEnums;
|
||||
|
||||
export const Viewer = () => {
|
||||
const [cornerstoneLoaded, setCornerstoneLoaded] = useState(false);
|
||||
const volumeViewport1Ref = useRef<HTMLDivElement | null>(null);
|
||||
const volumeViewport2Ref = useRef<HTMLDivElement | null>(null);
|
||||
const volumeViewport3Ref = useRef<HTMLDivElement | null>(null);
|
||||
const stackViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
/**
|
||||
* 当前的index
|
||||
*/
|
||||
const [index, setIndex] = useState<number>(0);
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const SeriesInstanceUID = queryParams.get("SeriesInstanceUID");
|
||||
const StudyInstanceUID = queryParams.get("StudyInstanceUID");
|
||||
const imageIdsRef = useRef<string[]>();
|
||||
const volumeId = "volume";
|
||||
const wwwl = { windowCenter: 50, windowWidth: 850 };
|
||||
const [imageIds, setImageIds] = useState<string[]>();
|
||||
const renderingEngineRef = useRef<RenderingEngine>();
|
||||
const volumeId = SeriesInstanceUID;
|
||||
|
||||
useMultiResizeObserver(
|
||||
[
|
||||
stackViewportRef,
|
||||
volumeViewport1Ref,
|
||||
volumeViewport2Ref,
|
||||
volumeViewport3Ref,
|
||||
],
|
||||
() => renderingEngineRef.current?.resize()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const setImageOrderCache = async () => {
|
||||
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
||||
cornerstoneTools.addTool(CrosshairsTool);
|
||||
cornerstoneTools.addTool(WindowLevelTool);
|
||||
cornerstoneTools.addTool(ZoomTool);
|
||||
|
||||
const renderViewport = async () => {
|
||||
if (!StudyInstanceUID || !SeriesInstanceUID) return;
|
||||
if (!volumeViewport1Ref.current) return;
|
||||
if (!volumeViewport2Ref.current) return;
|
||||
if (!volumeViewport3Ref.current) return;
|
||||
if (!stackViewportRef.current) return;
|
||||
renderingEngineRef.current = new RenderingEngine(renderingEngineId);
|
||||
// imageIds此时由于流式加载for mpr,是错乱的图片顺序
|
||||
const imageIds = await createImageIdsAndCacheMetaData({
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
wadoRsRoot,
|
||||
});
|
||||
|
||||
// 这一步会对imageIds进行排序,如果不排序imageIds会错误乱,stackViewport顺序会错误
|
||||
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||
const volume = await volumeLoader.createAndCacheVolume(
|
||||
SeriesInstanceUID,
|
||||
{
|
||||
imageIds,
|
||||
});
|
||||
}
|
||||
);
|
||||
volume.load();
|
||||
imageIdsRef.current = volume.imageIds;
|
||||
|
||||
// 默认windowWidtth
|
||||
const { windowCenter, windowWidth } = volume.cornerstoneImageMetaData;
|
||||
console.log("默认窗宽/位: ", windowCenter, windowWidth);
|
||||
const volumeViewportInput: PublicViewportInput[] = [
|
||||
{
|
||||
viewportId: viewportIds[0],
|
||||
type: ViewportType.ORTHOGRAPHIC,
|
||||
element: volumeViewport1Ref.current,
|
||||
defaultOptions: {
|
||||
orientation: OrientationAxis.AXIAL,
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportId: viewportIds[1],
|
||||
type: ViewportType.ORTHOGRAPHIC,
|
||||
element: volumeViewport2Ref.current,
|
||||
defaultOptions: {
|
||||
orientation: OrientationAxis.SAGITTAL,
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportId: viewportIds[2],
|
||||
type: ViewportType.ORTHOGRAPHIC,
|
||||
element: volumeViewport3Ref.current,
|
||||
defaultOptions: {
|
||||
orientation: OrientationAxis.CORONAL,
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
];
|
||||
const stackViewportInput: PublicViewportInput = {
|
||||
viewportId: stackViewportId,
|
||||
type: ViewportType.STACK, // 用于 Stack 视图
|
||||
element: stackViewportRef.current,
|
||||
defaultOptions: {
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
};
|
||||
|
||||
setCornerstoneLoaded(true);
|
||||
renderingEngineRef.current.setViewports([
|
||||
...volumeViewportInput,
|
||||
stackViewportInput,
|
||||
]);
|
||||
const stackViewport = renderingEngineRef.current.getViewport(
|
||||
stackViewportId
|
||||
) as IStackViewport;
|
||||
|
||||
stackViewport.setStack(volume.imageIds);
|
||||
setImageIds(volume.imageIds);
|
||||
|
||||
const toolGroupMpr = ToolGroupManager.createToolGroup(toolGroupMprId);
|
||||
if (toolGroupMpr) {
|
||||
viewportIds.forEach((vp) =>
|
||||
toolGroupMpr.addViewport(vp, renderingEngineId)
|
||||
);
|
||||
toolGroupMpr.addTool(ZoomTool.toolName);
|
||||
toolGroupMpr.addTool(WindowLevelTool.toolName);
|
||||
toolGroupMpr.addTool(StackScrollMouseWheelTool.toolName);
|
||||
toolGroupMpr.addTool(CrosshairsTool.toolName, {
|
||||
getReferenceLineColor,
|
||||
getReferenceLineControllable,
|
||||
getReferenceLineDraggableRotatable,
|
||||
getReferenceLineSlabThicknessControlsOn,
|
||||
});
|
||||
toolGroupMpr.setToolActive(ZoomTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Secondary }],
|
||||
});
|
||||
toolGroupMpr.setToolActive(CrosshairsTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }],
|
||||
});
|
||||
toolGroupMpr.setToolActive(StackScrollMouseWheelTool.toolName);
|
||||
toolGroupMpr.setToolActive(WindowLevelTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Auxiliary }],
|
||||
});
|
||||
}
|
||||
|
||||
const toolGroupStack = ToolGroupManager.createToolGroup(toolGroupStackId);
|
||||
if (toolGroupStack) {
|
||||
toolGroupStack.addViewport(stackViewportId, renderingEngineId);
|
||||
toolGroupStack.addTool(ZoomTool.toolName);
|
||||
toolGroupStack.setToolActive(ZoomTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Secondary }],
|
||||
});
|
||||
toolGroupStack.addTool(WindowLevelTool.toolName);
|
||||
toolGroupStack.setToolActive(WindowLevelTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Auxiliary }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 先注册toolGroup,不然会找不到viewportInfo
|
||||
*/
|
||||
await setVolumesForViewports(
|
||||
renderingEngineRef.current,
|
||||
[{ volumeId: SeriesInstanceUID }],
|
||||
viewportIds
|
||||
);
|
||||
};
|
||||
|
||||
initCornerstone(() => {
|
||||
setImageOrderCache();
|
||||
renderViewport();
|
||||
});
|
||||
|
||||
}, [SeriesInstanceUID, StudyInstanceUID]);
|
||||
return () => {
|
||||
cache.purgeCache();
|
||||
ToolGroupManager.destroy();
|
||||
renderingEngineRef.current?.destroy();
|
||||
|
||||
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
|
||||
cornerstoneTools.removeTool(CrosshairsTool);
|
||||
cornerstoneTools.removeTool(WindowLevelTool);
|
||||
cornerstoneTools.removeTool(ZoomTool);
|
||||
};
|
||||
}, [SeriesInstanceUID, StudyInstanceUID, volumeId]);
|
||||
|
||||
const onChangeIndex = (value: number[]) => setIndex(value[0]);
|
||||
|
||||
/**
|
||||
* 滚轮换图逻辑
|
||||
*/
|
||||
useEffect(() => {
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
console.log("wheel");
|
||||
if (!imageIds) return;
|
||||
const delta = event.deltaY > 0 ? -1 : 1;
|
||||
if (delta === -1 && index === 0) return;
|
||||
if (delta === 1 && index === imageIds?.length - 1) return;
|
||||
setIndex((p) => p + delta);
|
||||
};
|
||||
const stackElement = stackViewportRef.current;
|
||||
stackElement?.addEventListener("wheel", handleWheel);
|
||||
return () => {
|
||||
stackElement?.removeEventListener("wheel", handleWheel);
|
||||
};
|
||||
}, [imageIds, index]);
|
||||
|
||||
// stack换图
|
||||
useEffect(() => {
|
||||
const engine = renderingEngineRef.current;
|
||||
if (engine) {
|
||||
const viewport = engine.getViewport(stackViewportId) as IStackViewport;
|
||||
viewport?.setImageIdIndex(index);
|
||||
}
|
||||
}, [index]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
{/* <div className="flex-shrink-0 border-b border-secondary">
|
||||
<div className="flex-shrink-0 border-b border-secondary">
|
||||
<ToolBarMenu />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={50}>
|
||||
|
@ -66,24 +267,49 @@ export const Viewer = () => {
|
|||
className="w-full h-full"
|
||||
>
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<div className="w-full h-full">
|
||||
{cornerstoneLoaded && imageIdsRef.current && (
|
||||
<StackViewer imageIds={imageIdsRef.current} />
|
||||
<div className="w-full h-full flex flex-col relative">
|
||||
<div ref={stackViewportRef} className="w-full flex-grow" />
|
||||
{imageIds && (
|
||||
<div className="absolute left-0 right-0 top-0 text-end">
|
||||
<Slider
|
||||
value={[index]}
|
||||
min={0}
|
||||
max={imageIds.length - 1}
|
||||
step={1}
|
||||
className="stack-slider"
|
||||
onValueChange={onChangeIndex}
|
||||
/>
|
||||
<div className="text-xs">
|
||||
{index + 1}/{imageIds.length}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizableHandle withHandle={false} />
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<div>bototm</div>
|
||||
<div className="w-full h-full">
|
||||
<Model3DViewer />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={50}>
|
||||
{cornerstoneLoaded && imageIdsRef.current && (
|
||||
<CrosshairMpr wwwl={wwwl} volumeId={volumeId} />
|
||||
)}
|
||||
<ResizablePanelGroup direction="vertical" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={1 / 3}>
|
||||
<div className="h-full" ref={volumeViewport1Ref} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={1 / 3}>
|
||||
<div className="h-full" ref={volumeViewport2Ref} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={1 / 3}>
|
||||
<div className="h-full" ref={volumeViewport3Ref} />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
|
41
apps/desktop/src/pages/Viewer/useMultiResizeObserver.tsx
Normal file
41
apps/desktop/src/pages/Viewer/useMultiResizeObserver.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useEffect, RefObject } from "react";
|
||||
|
||||
type ResizeObserverCallback = () => void;
|
||||
|
||||
/**
|
||||
* 自定义 Hook,用于监听多个 Ref 对象的尺寸变化
|
||||
* @param refs - 要监听的 Ref 对象数组
|
||||
* @param callback - 尺寸变化时调用的回调函数
|
||||
*/
|
||||
function useMultiResizeObserver(
|
||||
refs: Array<RefObject<Element>>,
|
||||
callback: ResizeObserverCallback
|
||||
) {
|
||||
useEffect(() => {
|
||||
const elements = refs
|
||||
.map((ref) => ref.current)
|
||||
.filter((el): el is Element => el !== null);
|
||||
|
||||
if (elements.length === 0) return;
|
||||
|
||||
let resizeTimeout: NodeJS.Timeout | null = null;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(callback, 100);
|
||||
});
|
||||
|
||||
elements.forEach((element) => {
|
||||
resizeObserver.observe(element);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
elements.forEach((element) => {
|
||||
resizeObserver.unobserve(element);
|
||||
});
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [refs, callback]);
|
||||
}
|
||||
|
||||
export default useMultiResizeObserver;
|
|
@ -34,4 +34,11 @@ export default defineConfig({
|
|||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
cors: true,
|
||||
headers: {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user