feat: stack viewport
This commit is contained in:
parent
6c6d2178a0
commit
3226b47790
|
@ -12,6 +12,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.4.0",
|
"@ant-design/icons": "^5.4.0",
|
||||||
|
"@cornerstonejs/calculate-suv": "1.1.0",
|
||||||
|
"@cornerstonejs/core": "1.84.4",
|
||||||
|
"@cornerstonejs/dicom-image-loader": "1.84.4",
|
||||||
|
"@cornerstonejs/streaming-image-volume-loader": "1.84.4",
|
||||||
|
"@cornerstonejs/tools": "1.84.4",
|
||||||
"@google-cloud/spanner": "^7.12.0",
|
"@google-cloud/spanner": "^7.12.0",
|
||||||
"@hookform/resolvers": "3.9.0",
|
"@hookform/resolvers": "3.9.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
|
@ -23,6 +28,7 @@
|
||||||
"@radix-ui/react-radio-group": "^1.2.0",
|
"@radix-ui/react-radio-group": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
|
"@radix-ui/react-slider": "^1.2.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
|
@ -31,19 +37,25 @@
|
||||||
"@types/react-icons": "^3.0.0",
|
"@types/react-icons": "^3.0.0",
|
||||||
"@xenova/transformers": "^2.17.2",
|
"@xenova/transformers": "^2.17.2",
|
||||||
"antd": "^5.20.0",
|
"antd": "^5.20.0",
|
||||||
|
"axios": "1.7.7",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"custom-electron-titlebar": "^4.2.8",
|
"custom-electron-titlebar": "^4.2.8",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
|
"dcmjs": "0.34.1",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dicom-parser": "1.8.21",
|
"dicom-parser": "1.8.21",
|
||||||
|
"dicomweb-client": "0.10.4",
|
||||||
"dockview": "^1.15.2",
|
"dockview": "^1.15.2",
|
||||||
|
"electron-log": "5.2.0",
|
||||||
"electron-store": "^10.0.0",
|
"electron-store": "^10.0.0",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"embla-carousel-react": "^8.2.0",
|
||||||
"flexlayout-react": "^0.7.15",
|
"flexlayout-react": "^0.7.15",
|
||||||
|
"form-data": "4.0.0",
|
||||||
"framer-motion": "^11.3.24",
|
"framer-motion": "^11.3.24",
|
||||||
|
"lodash": "4.17.21",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"lucide-react": "^0.408.0",
|
"lucide-react": "^0.408.0",
|
||||||
"node-machine-id": "1.1.12",
|
"node-machine-id": "1.1.12",
|
||||||
|
@ -55,29 +67,19 @@
|
||||||
"react-desktop": "^0.3.9",
|
"react-desktop": "^0.3.9",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
|
"react-grid-layout": "1.4.4",
|
||||||
"react-hook-form": "7.53.0",
|
"react-hook-form": "7.53.0",
|
||||||
"react-icons": "^5.2.1",
|
"react-icons": "^5.2.1",
|
||||||
|
"react-resizable": "3.0.5",
|
||||||
"react-resizable-panels": "^2.1.1",
|
"react-resizable-panels": "^2.1.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "3.23.8",
|
"zod": "3.23.8"
|
||||||
"axios": "1.7.7",
|
|
||||||
"lodash": "4.17.21",
|
|
||||||
"electron-log": "5.2.0",
|
|
||||||
"form-data": "4.0.0",
|
|
||||||
"@cornerstonejs/core": "1.84.4",
|
|
||||||
"@cornerstonejs/tools": "1.84.4",
|
|
||||||
"@cornerstonejs/dicom-image-loader": "1.84.4",
|
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "1.84.4",
|
|
||||||
"dicomweb-client": "0.10.4",
|
|
||||||
"@cornerstonejs/calculate-suv": "1.1.0",
|
|
||||||
"dcmjs": "0.34.1",
|
|
||||||
"react-grid-layout": "1.4.4",
|
|
||||||
"react-resizable": "3.0.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@types/lodash": "4.17.7",
|
||||||
"@types/node": "22.5.2",
|
"@types/node": "22.5.2",
|
||||||
"@types/react": "^18.2.64",
|
"@types/react": "^18.2.64",
|
||||||
"@types/react-dom": "^18.2.21",
|
"@types/react-dom": "^18.2.21",
|
||||||
|
@ -96,7 +98,6 @@
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.1.6",
|
||||||
"vite-plugin-electron": "^0.28.6",
|
"vite-plugin-electron": "^0.28.6",
|
||||||
"vite-plugin-electron-renderer": "^0.14.5",
|
"vite-plugin-electron-renderer": "^0.14.5",
|
||||||
"@types/lodash": "4.17.7",
|
|
||||||
"vite-plugin-node-polyfills": "0.22.0"
|
"vite-plugin-node-polyfills": "0.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
28
apps/desktop/src/components/ui/slider.tsx
Normal file
28
apps/desktop/src/components/ui/slider.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
))
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Slider }
|
|
@ -22,6 +22,7 @@ interface CreateImageIdsAndCacheMetaDataOptions {
|
||||||
SOPInstanceUID?: string | null;
|
SOPInstanceUID?: string | null;
|
||||||
wadoRsRoot: string;
|
wadoRsRoot: string;
|
||||||
client?: api.DICOMwebClient | null;
|
client?: api.DICOMwebClient | null;
|
||||||
|
convertMultiframe?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,6 +71,7 @@ export const createImageIdsAndCacheMetaData = async (
|
||||||
return imageId;
|
return imageId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (convertMultiframeImageIds)
|
||||||
imageIds = convertMultiframeImageIds(imageIds);
|
imageIds = convertMultiframeImageIds(imageIds);
|
||||||
|
|
||||||
imageIds.forEach((imageId) => {
|
imageIds.forEach((imageId) => {
|
||||||
|
@ -84,11 +86,8 @@ export const createImageIdsAndCacheMetaData = async (
|
||||||
const pixelSpacing = getPixelSpacingInformation(metadata) as Number[];
|
const pixelSpacing = getPixelSpacingInformation(metadata) as Number[];
|
||||||
|
|
||||||
if (pixelSpacing) {
|
if (pixelSpacing) {
|
||||||
// FIXME: cornerstone类型定义有问题,这里.add方法缺少type属性
|
|
||||||
calibratedPixelSpacingMetadataProvider.add(imageId, {
|
calibratedPixelSpacingMetadataProvider.add(imageId, {
|
||||||
// @ts-ignore
|
|
||||||
rowPixelSpacing: pixelSpacing[0],
|
rowPixelSpacing: pixelSpacing[0],
|
||||||
// @ts-ignore
|
|
||||||
columnPixelSpacing: pixelSpacing[1],
|
columnPixelSpacing: pixelSpacing[1],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import * as cornerstoneTools from "@cornerstonejs/tools";
|
import * as cornerstoneTools from "@cornerstonejs/tools";
|
||||||
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
||||||
import { createImageIdsAndCacheMetaData } from "./CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
|
|
||||||
import setCtTransferFunctionForVolumeActor from "./CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor";
|
import setCtTransferFunctionForVolumeActor from "./CornerstoneDicomLoader/setCtTransferFunctionForVolumeActor";
|
||||||
import {
|
import {
|
||||||
RenderingEngine,
|
RenderingEngine,
|
||||||
setVolumesForViewports,
|
setVolumesForViewports,
|
||||||
volumeLoader,
|
|
||||||
Enums as CoreEnums,
|
Enums as CoreEnums,
|
||||||
} from "@cornerstonejs/core";
|
} from "@cornerstonejs/core";
|
||||||
import {
|
import {
|
||||||
|
@ -19,7 +17,6 @@ import {
|
||||||
viewportId3,
|
viewportId3,
|
||||||
ViewportId,
|
ViewportId,
|
||||||
} from "./Crosshair.config";
|
} from "./Crosshair.config";
|
||||||
import { IStackViewport } from "@cornerstonejs/core/dist/types/types";
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ToolGroupManager,
|
ToolGroupManager,
|
||||||
|
@ -34,10 +31,6 @@ const { MouseBindings } = csToolsEnums;
|
||||||
|
|
||||||
const { ViewportType } = CoreEnums;
|
const { ViewportType } = CoreEnums;
|
||||||
|
|
||||||
interface CrosshairMprProps {
|
|
||||||
children?: JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReferenceLineColor(vpId: ViewportId) {
|
function getReferenceLineColor(vpId: ViewportId) {
|
||||||
return viewportColors[vpId];
|
return viewportColors[vpId];
|
||||||
}
|
}
|
||||||
|
@ -58,23 +51,25 @@ function getReferenceLineSlabThicknessControlsOn(vpId: ViewportId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CrosshairMprProps {
|
interface CrosshairMprProps {
|
||||||
StudyInstanceUID: string;
|
wwwl: {
|
||||||
SeriesInstanceUID: string;
|
windowCenter: number;
|
||||||
|
windowWidth: number;
|
||||||
|
};
|
||||||
|
volumeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CrosshairMpr = (props: CrosshairMprProps) => {
|
export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const viewportStack = useRef<HTMLDivElement | null>(null);
|
|
||||||
const viewportRef_AXIAL = useRef<HTMLDivElement | null>(null);
|
const viewportRef_AXIAL = useRef<HTMLDivElement | null>(null);
|
||||||
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
|
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
|
||||||
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
|
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
|
||||||
const renderingEngine = useRef<RenderingEngine>();
|
const renderingEngine = useRef<RenderingEngine>();
|
||||||
const toolGroupIdRef = useRef<string>("");
|
|
||||||
const toolGroupRef = useRef<cornerstoneTools.Types.IToolGroup | undefined>(
|
const toolGroupRef = useRef<cornerstoneTools.Types.IToolGroup | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
const imageIds = useRef<string[]>();
|
|
||||||
const ts = "-" + Date.now();
|
const ts = "-" + Date.now();
|
||||||
|
const toolGroupId = "mprToolGroup" + ts;
|
||||||
|
const renderingEngineId = "mprRenderingEngine" + ts;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
||||||
|
@ -82,8 +77,6 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
cornerstoneTools.addTool(WindowLevelTool);
|
cornerstoneTools.addTool(WindowLevelTool);
|
||||||
cornerstoneTools.addTool(ZoomTool);
|
cornerstoneTools.addTool(ZoomTool);
|
||||||
|
|
||||||
const toolGroupId = props.SeriesInstanceUID + ts;
|
|
||||||
toolGroupIdRef.current = toolGroupId;
|
|
||||||
toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId);
|
toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId);
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
@ -94,40 +87,7 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
renderingEngine.current = new RenderingEngine(
|
renderingEngine.current = new RenderingEngine(renderingEngineId);
|
||||||
props.SeriesInstanceUID + ts
|
|
||||||
);
|
|
||||||
|
|
||||||
imageIds.current = await createImageIdsAndCacheMetaData({
|
|
||||||
StudyInstanceUID: props.StudyInstanceUID,
|
|
||||||
SeriesInstanceUID: props.SeriesInstanceUID,
|
|
||||||
wadoRsRoot: "http://localhost:8042/dicom-web",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define a volume in memory
|
|
||||||
const volume = await volumeLoader.createAndCacheVolume(
|
|
||||||
props.SeriesInstanceUID + ts,
|
|
||||||
{
|
|
||||||
imageIds: imageIds.current ?? [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 默认windowWidtth
|
|
||||||
const { windowCenter, windowWidth } = volume.cornerstoneImageMetaData;
|
|
||||||
const defaultWindowCenter = 50;
|
|
||||||
const defaultWindowWidth = 850;
|
|
||||||
console.log("采用默认窗宽/位: ", windowCenter, windowWidth);
|
|
||||||
|
|
||||||
// 定义 Stack 视口
|
|
||||||
const stackViewport = "stack" + props.SeriesInstanceUID + ts;
|
|
||||||
const viewportInput: PublicViewportInput = {
|
|
||||||
viewportId: stackViewport,
|
|
||||||
type: CoreEnums.ViewportType.STACK, // 用于 Stack 视图
|
|
||||||
element: viewportStack.current!,
|
|
||||||
defaultOptions: {
|
|
||||||
background: [0, 0, 0],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the viewports
|
// Create the viewports
|
||||||
const viewportInputArray: PublicViewportInput[] = [
|
const viewportInputArray: PublicViewportInput[] = [
|
||||||
|
@ -160,31 +120,19 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
renderingEngine.current.setViewports([
|
renderingEngine.current.setViewports(viewportInputArray);
|
||||||
...viewportInputArray,
|
|
||||||
viewportInput,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const viewport = renderingEngine.current.getViewport(
|
|
||||||
stackViewport
|
|
||||||
) as IStackViewport;
|
|
||||||
|
|
||||||
viewport.setStack(imageIds.current, 80);
|
|
||||||
|
|
||||||
// Set the volume to load
|
|
||||||
volume.load();
|
|
||||||
|
|
||||||
// Set volumes on the viewports
|
// Set volumes on the viewports
|
||||||
await setVolumesForViewports(
|
await setVolumesForViewports(
|
||||||
renderingEngine.current,
|
renderingEngine.current,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
volumeId: props.SeriesInstanceUID + ts,
|
volumeId: props.volumeId,
|
||||||
callback: ({ volumeActor }) =>
|
callback: ({ volumeActor }) =>
|
||||||
setCtTransferFunctionForVolumeActor({
|
setCtTransferFunctionForVolumeActor({
|
||||||
volumeActor,
|
volumeActor,
|
||||||
defaultWindowCenter,
|
defaultWindowCenter: props.wwwl.windowCenter,
|
||||||
defaultWindowWidth,
|
defaultWindowWidth: props.wwwl.windowWidth,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -194,18 +142,9 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
if (toolGroupRef.current) {
|
if (toolGroupRef.current) {
|
||||||
// For the crosshairs to operate, the viewports must currently be
|
// For the crosshairs to operate, the viewports must currently be
|
||||||
// added ahead of setting the tool active. This will be improved in the future.
|
// added ahead of setting the tool active. This will be improved in the future.
|
||||||
toolGroupRef.current.addViewport(
|
toolGroupRef.current.addViewport(viewportId1, renderingEngineId);
|
||||||
viewportId1,
|
toolGroupRef.current.addViewport(viewportId2, renderingEngineId);
|
||||||
props.SeriesInstanceUID + ts
|
toolGroupRef.current.addViewport(viewportId3, renderingEngineId);
|
||||||
);
|
|
||||||
toolGroupRef.current.addViewport(
|
|
||||||
viewportId2,
|
|
||||||
props.SeriesInstanceUID + ts
|
|
||||||
);
|
|
||||||
toolGroupRef.current.addViewport(
|
|
||||||
viewportId3,
|
|
||||||
props.SeriesInstanceUID + ts
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* zoom影像
|
* zoom影像
|
||||||
|
@ -256,9 +195,7 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.SeriesInstanceUID && props.StudyInstanceUID) {
|
|
||||||
run();
|
run();
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// 禁用视口
|
// 禁用视口
|
||||||
|
@ -270,7 +207,7 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
renderingEngine.current?.destroy();
|
renderingEngine.current?.destroy();
|
||||||
|
|
||||||
// 从 ToolGroupManager 中移除工具组
|
// 从 ToolGroupManager 中移除工具组
|
||||||
ToolGroupManager.destroyToolGroup(toolGroupIdRef.current);
|
ToolGroupManager.destroyToolGroup(toolGroupId);
|
||||||
|
|
||||||
// 移出工具注册
|
// 移出工具注册
|
||||||
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
|
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
|
||||||
|
@ -278,7 +215,7 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
cornerstoneTools.removeTool(WindowLevelTool);
|
cornerstoneTools.removeTool(WindowLevelTool);
|
||||||
cornerstoneTools.removeTool(ZoomTool);
|
cornerstoneTools.removeTool(ZoomTool);
|
||||||
};
|
};
|
||||||
}, [props, ts]);
|
}, [props, renderingEngineId, toolGroupId]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpr resize
|
* mpr resize
|
||||||
|
@ -288,7 +225,6 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
let resizeTimeout: NodeJS.Timeout | null = null;
|
let resizeTimeout: NodeJS.Timeout | null = null;
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
console.log("mpr resize");
|
|
||||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||||
resizeTimeout = setTimeout(() => renderingEngine.current?.resize(), 100);
|
resizeTimeout = setTimeout(() => renderingEngine.current?.resize(), 100);
|
||||||
});
|
});
|
||||||
|
@ -302,10 +238,9 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="w-full h-full flex flex-col">
|
<div ref={containerRef} className="w-full h-full flex flex-col">
|
||||||
<div className="w-full h-1/4" ref={viewportRef_AXIAL} />
|
<div className="w-full h-1/3" ref={viewportRef_AXIAL} />
|
||||||
<div className="w-full h-1/4" ref={viewportRef_SAGITTAL} />
|
<div className="w-full h-1/3" ref={viewportRef_SAGITTAL} />
|
||||||
<div className="w-full h-1/4" ref={viewportRef_CORONAL} />
|
<div className="w-full h-1/3" ref={viewportRef_CORONAL} />
|
||||||
<div className="w-full h-1/4" ref={viewportStack} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
79
apps/desktop/src/pages/Viewer/StackViewer/index.tsx
Normal file
79
apps/desktop/src/pages/Viewer/StackViewer/index.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -7,47 +7,56 @@ import {
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
import { ToolBarMenu } from "./MprViewer/ToolBarMenu";
|
import { StackViewer } from "./StackViewer";
|
||||||
import { RenderingEngine } from "@cornerstonejs/core";
|
import { createImageIdsAndCacheMetaData } from "./MprViewer/CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
|
||||||
|
import { volumeLoader } from "@cornerstonejs/core";
|
||||||
|
|
||||||
export interface CurrentDicom {
|
const wadoRsRoot = "http://localhost:8042/dicom-web";
|
||||||
SeriesInstanceUID: string | null;
|
|
||||||
StudyInstanceUID: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Viewer = () => {
|
export const Viewer = () => {
|
||||||
const [cornerstoneLoaded, setCornerstoneLoaded] = useState(false);
|
const [cornerstoneLoaded, setCornerstoneLoaded] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
const [currentDicom, setCurrentDicom] = useState<CurrentDicom>({
|
const SeriesInstanceUID = queryParams.get("SeriesInstanceUID");
|
||||||
SeriesInstanceUID: queryParams.get("SeriesInstanceUID"),
|
const StudyInstanceUID = queryParams.get("StudyInstanceUID");
|
||||||
StudyInstanceUID: queryParams.get("StudyInstanceUID"),
|
const imageIdsRef = useRef<string[]>();
|
||||||
|
const volumeId = "volume";
|
||||||
|
const wwwl = { windowCenter: 50, windowWidth: 850 };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setImageOrderCache = async () => {
|
||||||
|
if (!StudyInstanceUID || !SeriesInstanceUID) return;
|
||||||
|
// imageIds此时由于流式加载for mpr,是错乱的图片顺序
|
||||||
|
const imageIds = await createImageIdsAndCacheMetaData({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
wadoRsRoot,
|
||||||
});
|
});
|
||||||
|
// 这一步会对imageIds进行排序,如果不排序imageIds会错误乱,stackViewport顺序会错误
|
||||||
|
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||||
|
imageIds,
|
||||||
|
});
|
||||||
|
volume.load();
|
||||||
|
imageIdsRef.current = volume.imageIds;
|
||||||
|
|
||||||
useEffect(() => {
|
// 默认windowWidtth
|
||||||
const { SeriesInstanceUID, StudyInstanceUID } = currentDicom;
|
const { windowCenter, windowWidth } = volume.cornerstoneImageMetaData;
|
||||||
if (StudyInstanceUID && SeriesInstanceUID) {
|
console.log("默认窗宽/位: ", windowCenter, windowWidth);
|
||||||
const metadata = { SeriesInstanceUID, StudyInstanceUID };
|
|
||||||
localStorage.setItem("viewCache", JSON.stringify(metadata));
|
|
||||||
}
|
|
||||||
}, [currentDicom]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(window.location.href);
|
|
||||||
initCornerstone(() => {
|
|
||||||
setCornerstoneLoaded(true);
|
setCornerstoneLoaded(true);
|
||||||
});
|
};
|
||||||
}, [queryParams]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
initCornerstone(() => {
|
||||||
console.log(cornerstoneLoaded);
|
setImageOrderCache();
|
||||||
}, [cornerstoneLoaded]);
|
});
|
||||||
|
|
||||||
|
}, [SeriesInstanceUID, StudyInstanceUID]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col">
|
<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 />
|
<ToolBarMenu />
|
||||||
</div>
|
</div> */}
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||||
<ResizablePanel defaultSize={50}>
|
<ResizablePanel defaultSize={50}>
|
||||||
|
@ -57,7 +66,11 @@ export const Viewer = () => {
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
>
|
>
|
||||||
<ResizablePanel defaultSize={50}>
|
<ResizablePanel defaultSize={50}>
|
||||||
<div id="stackElement" className="w-full h-full"></div>
|
<div className="w-full h-full">
|
||||||
|
{cornerstoneLoaded && imageIdsRef.current && (
|
||||||
|
<StackViewer imageIds={imageIdsRef.current} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel defaultSize={50}>
|
<ResizablePanel defaultSize={50}>
|
||||||
|
@ -68,13 +81,8 @@ export const Viewer = () => {
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel defaultSize={50}>
|
<ResizablePanel defaultSize={50}>
|
||||||
{cornerstoneLoaded &&
|
{cornerstoneLoaded && imageIdsRef.current && (
|
||||||
currentDicom.StudyInstanceUID &&
|
<CrosshairMpr wwwl={wwwl} volumeId={volumeId} />
|
||||||
currentDicom.SeriesInstanceUID && (
|
|
||||||
<CrosshairMpr
|
|
||||||
StudyInstanceUID={currentDicom.StudyInstanceUID}
|
|
||||||
SeriesInstanceUID={currentDicom.SeriesInstanceUID}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|
|
@ -61,6 +61,9 @@ importers:
|
||||||
'@radix-ui/react-select':
|
'@radix-ui/react-select':
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-slider':
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
version: 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
@ -1742,6 +1745,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.2.0':
|
||||||
|
resolution: {integrity: sha512-dAHCDA4/ySXROEPaRtaMV5WHL8+JB/DbtyTbJjYkY0RXmKMO2Ln8DFZhywG5/mVQ4WqHDBc8smc14yPXPqZHYA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.0.2':
|
'@radix-ui/react-slot@1.0.2':
|
||||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -7233,6 +7249,25 @@ snapshots:
|
||||||
'@types/react': 18.3.4
|
'@types/react': 18.3.4
|
||||||
'@types/react-dom': 18.3.0
|
'@types/react-dom': 18.3.0
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/number': 1.1.0
|
||||||
|
'@radix-ui/primitive': 1.1.0
|
||||||
|
'@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.4)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.4
|
||||||
|
'@types/react-dom': 18.3.0
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.0.2(@types/react@18.3.4)(react@18.3.1)':
|
'@radix-ui/react-slot@1.0.2(@types/react@18.3.4)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.25.4
|
'@babel/runtime': 7.25.4
|
||||||
|
|
Loading…
Reference in New Issue
Block a user