import React, { useEffect, useRef } from 'react'; import * as cornerstone from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; import * as cornerstoneTools from '@cornerstonejs/tools'; import { annotation, SplineROITool } from '@cornerstonejs/tools'; import { eventTarget } from '@cornerstonejs/core'; import { registerGlobalTools } from '@/utils/cornerstoneToolsSetup'; import { MeasurementToolManager } from '@/utils/measurementToolManager'; import TibialPlateauAngleTool from '@/components/measures/TibialPlateauAngleTool'; import DARAMeasurementTool from '@/components/measures/DARAMeasurementTool'; import HipDIMeasurementTool from '@/components/measures/HipDIMeasurementTool'; import HipNHAAngleMeasurementTool from '@/components/measures/HipNHAAngleMeasurementTool'; import VHSMeasurementTool from '@/components/measures/VHSMeasurementTool'; import TPLOMeasurementTool from '@/components/measures/TPLOMeasurementTool'; import TTAMeasurementTool from '@/components/measures/TTAMeasurementTool'; import CBLOMeasurementTool from '@/components/measures/CBLOMeasurementTool'; import HipCoverageMeasurementTool from '@/components/measures/HipCoverageMeasurementTool'; import HipDorsalCoverageTool from '@/components/measures/HipDorsalCoverageTool'; import CircleCenterMeasurementTool from '@/components/measures/CircleCenterMeasurementTool'; import MidlineMeasurementTool from '@/components/measures/MidlineMeasurementTool'; import FindMidpointMeasurementTool from '@/components/measures/FindMidpointMeasurementTool'; import VerticalTiltMeasurementTool from '@/components/measures/VerticalTiltMeasurementTool'; import HorizontalTiltMeasurementTool from '@/components/measures/HorizontalTiltMeasurementTool'; import LineGrayscaleMeasurementTool from '@/components/measures/LineGrayscaleMeasurementTool'; import RectangleGrayscaleMeasurementTool from '@/components/measures/RectangleGrayscaleMeasurementTool'; import MaskTool from '@/components/tools/MaskTool'; import DicomOverlayTool from '@/components/overlay/DicomOverlayTool'; import ImageViewerErrorBoundary from './ImageViewerErrorBoundary'; import { boolean } from 'zod'; import { EVENTS } from '@cornerstonejs/core'; import { useSelector, useDispatch } from 'react-redux'; import { selectOverlayEnabled } from '@/states/view/dicomOverlaySlice'; import { initPlayback, setFrameDetectionResult } from '@/states/view/playbackSlice'; import PolygonLengthMeasurementTool from '@/components/measures/PolygonLengthMeasurementTool'; import PolylineLengthMeasurementTool from '@/components/measures/PolylineLengthMeasurementTool'; import { PlaybackController } from '@/pages/view/components/playback/PlaybackController'; import { FloatingPlaybackControls } from '@/pages/view/components/playback/FloatingPlaybackControls'; import { getIpPort } from '@/API/config'; import { getRenderingEngine } from '@cornerstonejs/core'; const { MagnifyTool, PanTool, WindowLevelTool, StackScrollTool, ZoomTool, LabelTool, LengthTool, AngleTool,//角度测量工具 ToolGroupManager, Enums: csToolsEnums, PlanarRotateTool, } = cornerstoneTools; // 导入 cursors 用于光标管理 import { cursors } from '@cornerstonejs/tools'; import { message } from 'antd'; const { MouseBindings } = csToolsEnums; // 全局工具状态变化监听器注册标志 let cursorResetListenerRegistered = false; // let toolGroup: cornerstoneTools.Types.IToolGroup; // let currentViewportId: string; /** * 增强错误信息,添加更多上下文 */ function enhanceError( error: unknown, context: { imageUrls: string[]; imageIndex: number; viewportId: string; } ): Error { const baseError = error instanceof Error ? error : new Error(String(error)); const { imageUrls, imageIndex, viewportId } = context; const contextInfo = { imageUrls, imageIndex, viewportId, timestamp: new Date().toISOString(), userAgent: navigator.userAgent }; // 创建增强的错误消息 const enhancedMessage = `[${context.viewportId}] 图像加载失败: ${baseError.message} 上下文信息: ${JSON.stringify(contextInfo, null, 2)}`; const enhancedError = new Error(enhancedMessage); enhancedError.name = `ImageLoadError`; enhancedError.stack = `${enhancedError.name}: ${enhancedMessage}\n原错误堆栈: ${baseError.stack}`; return enhancedError; } /** * 判断是否为严重错误,需要抛出给 Error Boundary */ function isCriticalError(error: Error): boolean { const message = error.message.toLowerCase(); // 以下类型的错误被认为是严重的,需要用户干预 const criticalErrors = [ '图像url列表为空', '图像索引无效', '401', // 认证失败 '403', // 权限不足 '404', // 文件不存在 '500', // 服务器错误 '认证失败', '权限不足', 'unauthorized', 'forbidden', 'not found', 'internal server error' ]; return criticalErrors.some(keyword => message.includes(keyword)); } function getToolgroupByViewportId(currentViewportId: string) { const toolGroup = ToolGroupManager.getToolGroup( `STACK_TOOL_GROUP_ID_${currentViewportId}` ); if (!toolGroup) { console.log('toolGroup not found'); throw new Error('Tool group not found'); } return toolGroup; } function overlayRedRectangle(currentViewportId: string) { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; const viewportElement = viewport.element; const annotations = cornerstoneTools.annotation.state.getAnnotations( 'LinearSplineROI', viewportElement ); if (!annotations || annotations.length === 0) { console.log('No ROI annotations found'); return; } const annotation = annotations[annotations.length - 1]; const points = annotation?.data?.handles?.points; // 创建一个覆盖 Canvas const canvas = document.createElement('canvas'); canvas.style.position = 'absolute'; canvas.width = viewportElement.clientWidth; canvas.height = viewportElement.clientHeight; viewportElement.firstChild?.appendChild(canvas); const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Failed to get 2D context from canvas'); } ctx.fillStyle = 'rgba(0, 0, 0, 1)'; // 将 ROI 坐标转换为 Canvas 坐标 if (!points) { console.log('No points found in handles'); return; } // Convert all points to canvas coordinates const z = viewport.getCurrentImageIdIndex() || 0; const canvasPoints = points.map((point: Types.Point2 | Types.Point3) => { const point3D: Types.Point3 = point.length === 2 ? [point[0], point[1], z] : point; return viewport.worldToCanvas(point3D); }); // Log for debugging console.log('Canvas Points:', canvasPoints); // Draw the polygon ctx.beginPath(); ctx.rect(0, 0, canvas.width, canvas.height); // Full canvas for evenodd rule ctx.moveTo(canvasPoints[0][0], canvasPoints[0][1]); // Start at the first point for (let i = 1; i < canvasPoints.length; i++) { ctx.lineTo(canvasPoints[i][0], canvasPoints[i][1]); // Connect to subsequent points } ctx.closePath(); // Close the polygon ctx.fill('evenodd'); // Fill with red } function registerTools(viewportId, renderingEngineId) { // 确保全局工具已注册(只会执行一次) registerGlobalTools(); // 创建该 viewport 的工具组 const toolGroupId = `STACK_TOOL_GROUP_ID_${viewportId}`; // 检查工具组是否已存在 let toolGroup = ToolGroupManager.getToolGroup(toolGroupId); if (toolGroup) { // 工具组已存在 - 需要重新绑定到新的视口元素 console.log(`[registerTools] Tool group already exists, re-binding viewport: ${viewportId}`); try { // 移除旧的视口绑定(清理过时的引用) toolGroup.removeViewports(renderingEngineId, viewportId); console.log(`[registerTools] Removed old viewport binding`); } catch (error) { console.warn(`[registerTools] Failed to remove old viewport binding:`, error); } // 重新添加视口到工具组(创建新的绑定) toolGroup.addViewport(viewportId, renderingEngineId); console.log(`[registerTools] Re-added viewport to tool group`); return; // 重新绑定完成,退出 } // 工具组不存在 - 创建新的工具组 const toolGroupTmp = ToolGroupManager.createToolGroup(toolGroupId); if (!toolGroupTmp) { console.error( `[registerTools] Failed to create tool group for viewport: ${viewportId}` ); return; } toolGroup = toolGroupTmp; // 添加工具到工具组 toolGroup.addTool(MagnifyTool.toolName); toolGroup.addTool(PanTool.toolName); toolGroup.addTool(WindowLevelTool.toolName); toolGroup.addTool(StackScrollTool.toolName); toolGroup.addTool(ZoomTool.toolName); toolGroup.addTool(LabelTool.toolName); toolGroup.addTool(PlanarRotateTool.toolName); toolGroup.addTool(LengthTool.toolName); // 添加线段测量工具 toolGroup.addTool(AngleTool.toolName); // 添加角度测量工具 toolGroup.addTool(TibialPlateauAngleTool.toolName); // 添加胫骨平台夹角测量工具 toolGroup.addTool(DARAMeasurementTool.toolName); // 添加髋臼水平角测量工具 toolGroup.addTool(HipDIMeasurementTool.toolName); // 添加髋关节牵引指数测量工具 toolGroup.addTool(HipNHAAngleMeasurementTool.toolName); // 添加髋关节水平角测量工具 toolGroup.addTool(VHSMeasurementTool.toolName); // 添加心锥比测量工具 toolGroup.addTool(TPLOMeasurementTool.toolName); // 添加TPLO测量工具 toolGroup.addTool(TTAMeasurementTool.toolName); // 添加TTA测量工具 toolGroup.addTool(CBLOMeasurementTool.toolName); // 添加CBLO测量工具 toolGroup.addTool(HipCoverageMeasurementTool.toolName); // 添加股骨头覆盖率测量工具 toolGroup.addTool(HipDorsalCoverageTool.toolName); // 添加髋臼背覆盖测量工具 toolGroup.addTool(CircleCenterMeasurementTool.toolName); // 添加找圆心测量工具 toolGroup.addTool(MidlineMeasurementTool.toolName); // 添加找中线测量工具 toolGroup.addTool(FindMidpointMeasurementTool.toolName); // 添加找中点测量工具 toolGroup.addTool(VerticalTiltMeasurementTool.toolName); // 添加直线垂直倾斜度测量工具 toolGroup.addTool(HorizontalTiltMeasurementTool.toolName); // 添加直线水平倾斜度测量工具 toolGroup.addTool(DicomOverlayTool.toolName); // 添加DICOM四角信息显示工具 toolGroup.addTool(PolygonLengthMeasurementTool.toolName); // 添加多边形长度测量工具 toolGroup.addTool(PolylineLengthMeasurementTool.toolName); // 添加拆线长度测量工具 toolGroup.addTool(LineGrayscaleMeasurementTool.toolName); // 添加直线灰度测量工具 toolGroup.addTool(RectangleGrayscaleMeasurementTool.toolName); // 添加矩形区域灰度测量工具 toolGroup.addTool(MaskTool.toolName); // 添加掩码工具 // 设置默认工具状态 setupDefaultToolStates(toolGroup); // 添加 viewport 到工具组 toolGroup.addViewport(viewportId, renderingEngineId); // 添加全局工具模式变化监听器,自动重置光标(当前项目中使用一对一映射) if (!cursorResetListenerRegistered) { eventTarget.addEventListener( cornerstoneTools.Enums.Events.TOOL_MODE_CHANGED, (event) => { // 检查是否有活动工具,当前一对一映射设计只关注全局活动工具状态 const allToolGroups = ToolGroupManager.getAllToolGroups(); let hasActiveTool = false; for (const toolGroup of allToolGroups) { if (toolGroup.getActivePrimaryMouseButtonTool()) { hasActiveTool = true; break; } } if (!hasActiveTool) { // 没有活动工具时,重置所有视口光标为默认(适用于当前一对一设计) for (const toolGroup of allToolGroups) { const viewportIds = toolGroup.getViewportIds(); viewportIds.forEach(viewportId => { try { const enabledElement = cornerstone.getEnabledElementByViewportId(viewportId); if (enabledElement?.viewport.element) { enabledElement.viewport.element.style.cursor = 'default'; } } catch (error) { // 忽略错误,可能视口已被销毁 } }); } } } ); cursorResetListenerRegistered = true; // 防止重复注册 } console.log(`[registerTools] Tools registered for viewport: ${viewportId}`); } /** * 设置默认工具状态 */ function setupDefaultToolStates(toolGroup: cornerstoneTools.Types.IToolGroup) { // 设置平移工具(中键) toolGroup.setToolActive(PanTool.toolName, { bindings: [ { mouseButton: MouseBindings.Auxiliary, // Middle Click }, ], }); // 设置缩放工具(右键) toolGroup.setToolActive(ZoomTool.toolName, { bindings: [ { mouseButton: MouseBindings.Secondary, // Right Click }, ], }); // 设置滚轮滚动工具 toolGroup.setToolActive(StackScrollTool.toolName, { bindings: [ { mouseButton: MouseBindings.Wheel, // Mouse Wheel }, ], }); // 其他工具默认为被动状态 toolGroup.setToolPassive(MagnifyTool.toolName); toolGroup.setToolPassive(WindowLevelTool.toolName); toolGroup.setToolPassive(LabelTool.toolName); toolGroup.setToolPassive(PlanarRotateTool.toolName); toolGroup.setToolPassive(LengthTool.toolName); toolGroup.setToolPassive(AngleTool.toolName); toolGroup.setToolPassive(TibialPlateauAngleTool.toolName); toolGroup.setToolPassive(DARAMeasurementTool.toolName); toolGroup.setToolPassive(HipDIMeasurementTool.toolName); toolGroup.setToolPassive(HipNHAAngleMeasurementTool.toolName); toolGroup.setToolPassive(VHSMeasurementTool.toolName); toolGroup.setToolPassive(TPLOMeasurementTool.toolName); toolGroup.setToolPassive(TTAMeasurementTool.toolName); toolGroup.setToolPassive(CBLOMeasurementTool.toolName); toolGroup.setToolPassive(HipCoverageMeasurementTool.toolName); toolGroup.setToolPassive(HipDorsalCoverageTool.toolName); toolGroup.setToolPassive(CircleCenterMeasurementTool.toolName); toolGroup.setToolPassive(MidlineMeasurementTool.toolName); toolGroup.setToolPassive(FindMidpointMeasurementTool.toolName); toolGroup.setToolPassive(VerticalTiltMeasurementTool.toolName); toolGroup.setToolPassive(HorizontalTiltMeasurementTool.toolName); toolGroup.setToolPassive(PolygonLengthMeasurementTool.toolName); toolGroup.setToolPassive(PolylineLengthMeasurementTool.toolName); toolGroup.setToolPassive(LineGrayscaleMeasurementTool.toolName); toolGroup.setToolPassive(RectangleGrayscaleMeasurementTool.toolName); toolGroup.setToolPassive(MaskTool.toolName); } export function addLMark(currentViewportId: string): void { // Implement the logic to add an L mark console.log('Adding L Mark viewport id : ', currentViewportId); const toolGroup = getToolgroupByViewportId(currentViewportId); // currentViewportId = viewportId; toolGroup.setToolActive(LabelTool.toolName, { bindings: [ // { // mouseButton: MouseBindings.Primary, // Left Click // }, ], }); const position: Types.Point3 = [100, 100, 0]; // Example position const text = 'L'; // Predefined text LabelTool.hydrate(currentViewportId, position, text); toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true, }); // const enabledElement = cornerstone.getEnabledElementByViewportId(currentViewportId); // cursors.elementCursor.resetElementCursor(elementRef.current as HTMLDivElement); } export function addRLabel(currentViewportId: string): void { console.log('Adding R Mark viewport id : ', currentViewportId); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolActive(LabelTool.toolName, { bindings: [ // { // mouseButton: MouseBindings.Primary, // Left Click // }, ], }); // R标记相对于L标记向右偏移一些距离 const position: Types.Point3 = [300, 100, 0]; // 在L标记右侧200像素位置 const text = 'R'; // Predefined text LabelTool.hydrate(currentViewportId, position, text); toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true, }); } /** * 防抖工具函数 * @param func 要防抖的函数 * @param wait 等待时间(毫秒) * @returns 防抖后的函数 */ function debounce any>(func: T, wait: number): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; return (...args: Parameters) => { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function containImage(viewport: cornerstone.StackViewport) { viewport.getRenderingEngine().resize(/* immediate */ true); console.log(`使用 resetCamera方式`) viewport.resetCamera({ resetPan: true, resetZoom: true }); // 重置 pan/zoom 到 contain viewport.render(); } /** * 保持宽高比的图像适应函数 * 确保图像完全显示在容器内,同时保持原始宽高比(可能会有黑边) * @param currentViewportId 当前视口ID */ export function fitImageWithAspectRatio(currentViewportId: string): void { try { console.log(`[fitImageWithAspectRatio] 重新适配大小`) const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; if (!viewport) { console.warn(`[fitImageWithAspectRatio] Viewport not found: ${currentViewportId}`); return; } containImage(viewport) } catch (error) { console.error(`[fitImageWithAspectRatio] Error fitting image with aspect ratio:`, error); } } export function fitImageSize(currentViewportId: string) { // 使用新的保持宽高比的适应函数 fitImageWithAspectRatio(currentViewportId); } export function deleteSelectedMark(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId).viewport; const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations(); // 删除所有 LabelTool 创建的标记(包括 L/R 标记、预定义标记、自定义标记、时间戳标记) for (const annotation of allAnnotations) { if (annotation.metadata?.toolName === LabelTool.toolName) { cornerstoneTools.annotation.state.removeAnnotation( annotation.annotationUID! ); } } viewport.render(); } export function addMark(currentViewportId: string): void { // Implement the logic to add a mask console.log('Adding Mask'); // Add the specific logic to add a mask here cornerstoneTools.addTool(SplineROITool); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.addTool(SplineROITool.toolName); toolGroup.addToolInstance('LinearSplineROI', SplineROITool.toolName, { spline: { type: SplineROITool.SplineTypes.Linear, }, }); toolGroup.setToolActive('LinearSplineROI', { bindings: [{ mouseButton: MouseBindings.Primary }], }); } /** * 添加自定义文本标记到图像 */ export function addCustomMark(currentViewportId: string, text: string): void { console.log(`Adding custom mark: ${text} to viewport: ${currentViewportId}`); const toolGroup = getToolgroupByViewportId(currentViewportId); // 激活标签工具 toolGroup.setToolActive(LabelTool.toolName, { bindings: [], }); // 获取视口元素和位置 const element = document.getElementById(currentViewportId); if (!element) { console.error(`Element not found for viewport: ${currentViewportId}`); return; } const renderingEngine = getRenderingEngine("myRenderingEngine"); if (renderingEngine === undefined) { console.error(`Rendering engine not found for viewport: ${currentViewportId}`); message.error(`添加注释失败`); return; } const viewport = renderingEngine.getViewport(currentViewportId); // 计算标记位置(图像中心位置) // 1️⃣ canvas 坐标(屏幕中心) const { width, height } = element.getBoundingClientRect(); const canvasPoint: Types.Point2 = [width / 2, height / 2]; // 2️⃣ 转为 world 坐标(关键!) const worldPoint = viewport.canvasToWorld(canvasPoint); // 3️⃣ 使用 world 坐标 添加标签 LabelTool.hydrate(currentViewportId, worldPoint, text); // 恢复工具状态 toolGroup.setToolPassive(LabelTool.toolName, { removeAllBindings: true, }); console.log(`Custom mark "${text}" added successfully`); } export function remoteMask(currentViewportId: string): void { // 1. 获取所有 annotation const all = annotation.state.getAllAnnotations(); // 2. 过滤出 LinearSplineROI 产生的 const toRemove = all.filter( (a) => a.metadata?.toolName === 'LinearSplineROI' ); // 3. 逐条删掉 toRemove.forEach((a) => { if (a.annotationUID) { annotation.state.removeAnnotation(a.annotationUID); } }); const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; viewport.render(); console.log('Deleting Digital Mask'); const viewportElement = viewport.element; const firstChild = viewportElement.firstChild; if (firstChild) { const canvasElements = Array.from(firstChild.childNodes).filter( (child): child is Element => child instanceof Element && child.tagName === 'CANVAS' ); canvasElements.slice(1).forEach((canvas) => { firstChild.removeChild(canvas); }); } } export function HorizontalFlip(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 切换水平翻转状态 const { flipHorizontal } = viewport.getCamera(); viewport.setCamera({ flipHorizontal: !flipHorizontal }); console.log('Flipping Image Horizontally'); } export function VerticalFlip(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 切换竖直翻转状态 const { flipVertical } = viewport.getCamera(); viewport.setCamera({ flipVertical: !flipVertical }); } export function RotateCounterclockwise90(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 获取当前相机 const camera = viewport.getCamera(); // 计算新的旋转角度(当前角度 + 90度) const newRotation = (camera.rotation ?? 0) + 90; // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度! const ninetyDegreesRadians = (90 * Math.PI) / 180; // 获取当前的viewUp向量 const currentViewUp = camera.viewUp || [0, 1, 0]; // 计算旋转后的viewUp向量(这才是正确的相对旋转) const newViewUp: [number, number, number] = [ currentViewUp[0] * Math.cos(ninetyDegreesRadians) - currentViewUp[1] * Math.sin(ninetyDegreesRadians), currentViewUp[0] * Math.sin(ninetyDegreesRadians) + currentViewUp[1] * Math.cos(ninetyDegreesRadians), 0, ]; // 设置新的相机参数 viewport.setCamera({ ...camera, viewUp: newViewUp, rotation: newRotation % 360, // 确保角度在0-359范围内 }); viewport.render(); console.log('Rotating Image Counterclockwise 90°'); } export function RotateClockwise90(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; const camera = viewport.getCamera(); // 计算新的旋转角度(当前角度 - 90度) const newRotation = (camera.rotation ?? 0) - 90; // 但计算viewUp向量时,我们应该使用90度的弧度,而不是新角度的弧度! const ninetyDegreesRadians = (90 * Math.PI) / 180; // 获取当前的viewUp向量 const currentViewUp = camera.viewUp || [0, 1, 0]; // 计算顺时针旋转90度的viewUp向量(与逆时针相反) const newViewUp: [number, number, number] = [ currentViewUp[0] * Math.cos(ninetyDegreesRadians) + currentViewUp[1] * Math.sin(ninetyDegreesRadians), -currentViewUp[0] * Math.sin(ninetyDegreesRadians) + currentViewUp[1] * Math.cos(ninetyDegreesRadians), 0, ]; // 设置新的相机参数 viewport.setCamera({ ...camera, viewUp: newViewUp, rotation: newRotation % 360, // 确保角度在0-359范围内 }); viewport.render(); console.log('Rotating Image Clockwise 90°'); } export function ResetImage(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 获取当前图像ID以访问原始元数据 const currentImageId = viewport.getCurrentImageId(); // 计算正确的原始invert值 // 不能直接使用 viewport.getProperties().invert,因为"反色对比"功能会修改它 // 需要从图像的原始元数据中获取 photometricInterpretation let correctOriginalInvert = false; // 默认值 try { // 从Cornerstone缓存中获取图像对象 const image = cornerstone.cache.getImage(currentImageId) as any; if (image) { // 方法1: 从图像的metadata中获取photometricInterpretation const photometricInterpretation = image.photometricInterpretation || image.metadata?.dicomMetadata?.image?.photometricInterpretation || image.imageFrame?.photometricInterpretation; // MONOCHROME1需要invert=true才能正确显示 if (photometricInterpretation === 'MONOCHROME1') { correctOriginalInvert = true; } console.log('从图像元数据解析原始invert值', { imageId: currentImageId, photometricInterpretation, correctOriginalInvert }); } else { console.warn('无法从缓存获取图像,使用默认invert值'); } } catch (error) { console.warn('获取图像元数据失败,使用默认invert值', error); } // 重置相机设置(缩放、平移、旋转等) viewport.resetCamera(); // 重置视口属性 viewport.resetProperties(); // ⚠️ 重要发现:resetProperties() 后需要使用反转的 invert 值 // // 原因分析: // 1. 初始加载时:MONOCHROME1 图像使用 invert=true 正常显示 // 2. resetProperties() 后:渲染管线状态改变,需要 invert=false 才能正常显示 // 3. 这可能与 resetProperties() 重置了 VOI LUT 或其他影响渲染的属性有关 // // 因此: // - MONOCHROME1 (correctOriginalInvert=true) → 设置 invert=false // - MONOCHROME2 (correctOriginalInvert=false) → 设置 invert=true // // 这个行为虽然反直觉,但经过实际验证是正确的 viewport.setProperties({ invert: !correctOriginalInvert }); viewport.render(); console.log('Resetting Image', { imageId: currentImageId, restoredInvert: correctOriginalInvert }); } export function InvertImage(currentViewportId: string): void { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // Implement the logic to invert the image const invert = !viewport.getProperties().invert; viewport.setProperties({ invert }); viewport.render(); console.log('Inverting Image'); } export function setOriginalSize(currentViewportId: string) { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId) .viewport as cornerstone.StackViewport; // 1) 先reset到fit状态 viewport.resetCamera(); // 2) 计算像素级1:1需要的zoom(完全忽略spacing!) const { dimensions } = viewport.getImageData(); const canvas = viewport.canvas; // fit状态下,图像缩放到canvas的比例 const fitScale = Math.min( canvas.clientWidth / dimensions[0], canvas.clientHeight / dimensions[1] ); // 要达到像素级1:1,需要的zoom = 1 / fitScale const finalZoom = 1 / fitScale; console.log(`[setOriginalSize] Pixel-perfect 1:1 zoom calculation:`); console.log(` - Image dimensions: ${dimensions[0]}x${dimensions[1]}`); console.log(` - Canvas size: ${canvas.clientWidth}x${canvas.clientHeight}`); console.log(` - Fit scale: ${fitScale.toFixed(4)}`); console.log(` - Final zoom (1:1): ${finalZoom.toFixed(4)}`); viewport.setZoom(finalZoom); viewport.render(); } /** 停用 Magnifier 工具 */ export function deactivateMagnifier(currentViewportId: string) { console.log('Deactivating Magnifier'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolPassive(MagnifyTool.toolName, { removeAllBindings: true, }); } /** 激活 Magnifier 工具 */ export function activateMagnifier(currentViewportId: string) { console.log('Activating Magnifier'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolActive(MagnifyTool.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click }, ], }); } /** 激活 WindowLevel 工具 */ export function activateWindowLevel(currentViewportId: string) { const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolActive(WindowLevelTool.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click }, ], }); } /** 停用 WindowLevel 工具 */ export function deactivateWindowLevel(currentViewportId: string) { console.log('Deactivating WindowLevel Tool'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolPassive(WindowLevelTool.toolName, { removeAllBindings: true, }); } /** * 停用 Pan 工具 */ export function deactivatePan(currentViewportId: string) { console.log('Deactivating Pan Tool'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolPassive(PanTool.toolName, { removeAllBindings: true, }); } /** * 激活 Pan 工具 */ export function activatePan(currentViewportId: string) { console.log('Activating Pan Tool'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolActive(PanTool.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click }, ], }); } /** 停用 Zoom 工具 */ export function deactivateZoom(currentViewportId: string) { console.log('Deactivating Zoom Tool'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolPassive(ZoomTool.toolName, { removeAllBindings: true, }); } /** * 激活 Zoom 工具 */ export function activateZoom(currentViewportId: string) { console.log('Activating Zoom Tool'); const toolGroup = getToolgroupByViewportId(currentViewportId); toolGroup.setToolActive(ZoomTool.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click }, ], }); } export function invertContrast(currentViewportId: string) { const viewport = cornerstone.getEnabledElementByViewportId(currentViewportId).viewport; const targetBool = !viewport.getProperties().invert; viewport.setProperties({ invert: targetBool, }); viewport.render(); } export function rotateAnyAngle(currentViewportId: string) { const toolGroup = getToolgroupByViewportId(currentViewportId); const planar = toolGroup.getToolInstance(PlanarRotateTool.toolName); // Reset rotation angle const isActive = planar.mode === csToolsEnums.ToolModes.Active; console.log( `PlanarRotateTool is currently ${isActive ? 'active' : 'inactive'}` ); if (isActive) { toolGroup.setToolPassive(PlanarRotateTool.toolName, { removeAllBindings: true, }); } else { toolGroup.setToolActive(PlanarRotateTool.toolName, { bindings: [ { mouseButton: MouseBindings.Primary, // Left Click }, ], }); } console.log('Rotating Image by Any Angle'); } // ==================== 线段测量相关函数 ==================== /** * 激活线段测量工具 */ export function activateLengthMeasurement(viewportId: string): boolean { console.log( `[activateLengthMeasurement] Activating length measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateLengthTool(viewportId); } /** * 停用线段测量工具 */ export function deactivateLengthMeasurement(viewportId: string): boolean { console.log( `[deactivateLengthMeasurement] Deactivating length measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateLengthTool(viewportId); } /** * 切换线段测量工具状态 */ export function toggleLengthMeasurement(viewportId: string): boolean { console.log( `[toggleLengthMeasurement] Toggling length measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleLengthTool(viewportId); } /** * 清除线段测量标注 */ export function clearLengthMeasurements(viewportId: string): boolean { console.log( `[clearLengthMeasurements] Clearing length measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearLengthMeasurements(viewportId); } /** * 获取线段测量结果 */ // eslint-disable-next-line export function getLengthMeasurements(viewportId: string): any[] { console.log( `[getLengthMeasurements] Getting length measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getLengthMeasurements(viewportId); } /** * 检查线段测量工具是否激活 */ export function isLengthMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isLengthToolActive(viewportId); } // ==================== 角度测量相关函数 ==================== /** * 激活角度测量工具 */ export function activateAngleMeasurement(viewportId: string): boolean { console.log( `[activateAngleMeasurement] Activating angle measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateAngleTool(viewportId); } /** * 停用角度测量工具 */ export function deactivateAngleMeasurement(viewportId: string): boolean { console.log( `[deactivateAngleMeasurement] Deactivating angle measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateAngleTool(viewportId); } /** * 切换角度测量工具状态 */ export function toggleAngleMeasurement(viewportId: string): boolean { console.log( `[toggleAngleMeasurement] Toggling angle measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleAngleTool(viewportId); } /** * 清除角度测量标注 */ export function clearAngleMeasurements(viewportId: string): boolean { console.log( `[clearAngleMeasurements] Clearing angle measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearAngleMeasurements(viewportId); } /** * 获取角度测量结果 */ // eslint-disable-next-line export function getAngleMeasurements(viewportId: string): any[] { console.log( `[getAngleMeasurements] Getting angle measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getAngleMeasurements(viewportId); } /** * 检查角度测量工具是否激活 */ export function isAngleMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isAngleToolActive(viewportId); } // ==================== 胫骨平台夹角测量相关函数 ==================== /** * 激活胫骨平台夹角测量工具 */ export function activateTibialPlateauAngleMeasurement(viewportId: string): boolean { console.log( `[activateTibialPlateauAngleMeasurement] Activating TPA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateTibialPlateauAngleTool(viewportId); } /** * 停用胫骨平台夹角测量工具 */ export function deactivateTibialPlateauAngleMeasurement(viewportId: string): boolean { console.log( `[deactivateTibialPlateauAngleMeasurement] Deactivating TPA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateTibialPlateauAngleTool(viewportId); } /** * 切换胫骨平台夹角测量工具状态 */ export function toggleTibialPlateauAngleMeasurement(viewportId: string): boolean { console.log( `[toggleTibialPlateauAngleMeasurement] Toggling TPA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleTibialPlateauAngleTool(viewportId); } /** * 清除胫骨平台夹角测量标注 */ export function clearTibialPlateauAngleMeasurements(viewportId: string): boolean { console.log( `[clearTibialPlateauAngleMeasurements] Clearing TPA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearTibialPlateauAngleMeasurements(viewportId); } /** * 获取胫骨平台夹角测量结果 */ // eslint-disable-next-line export function getTibialPlateauAngleMeasurements(viewportId: string): any[] { console.log( `[getTibialPlateauAngleMeasurements] Getting TPA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getTibialPlateauAngleMeasurements(viewportId); } /** * 检查胫骨平台夹角测量工具是否激活 */ export function isTibialPlateauAngleMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isTibialPlateauAngleToolActive(viewportId); } // ==================== 髋臼水平角测量相关函数 ==================== /** * 激活髋臼水平角测量工具 */ export function activateDARAMeasurement(viewportId: string): boolean { console.log( `[activateDARAMeasurement] Activating DARA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateDARAMeasurementTool(viewportId); } /** * 停用髋臼水平角测量工具 */ export function deactivateDARAMeasurement(viewportId: string): boolean { console.log( `[deactivateDARAMeasurement] Deactivating DARA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateDARAMeasurementTool(viewportId); } /** * 切换髋臼水平角测量工具状态 */ export function toggleDARAMeasurement(viewportId: string): boolean { console.log( `[toggleDARAMeasurement] Toggling DARA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleDARAMeasurementTool(viewportId); } /** * 清除髋臼水平角测量标注 */ export function clearDARAMeasurements(viewportId: string): boolean { console.log( `[clearDARAMeasurements] Clearing DARA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearDARAMeasurements(viewportId); } /** * 获取髋臼水平角测量结果 */ // eslint-disable-next-line export function getDARAMeasurements(viewportId: string): any[] { console.log( `[getDARAMeasurements] Getting DARA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getDARAMeasurements(viewportId); } /** * 检查髋臼水平角测量工具是否激活 */ export function isDARAMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isDARAMeasurementToolActive(viewportId); } // ==================== 髋关节牵引指数测量相关函数 ==================== /** * 激活髋关节牵引指数测量工具 */ export function activateHipDIMeasurement(viewportId: string): boolean { console.log( `[activateHipDIMeasurement] Activating HipDI measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateHipDIMeasurementTool(viewportId); } /** * 停用髋关节牵引指数测量工具 */ export function deactivateHipDIMeasurement(viewportId: string): boolean { console.log( `[deactivateHipDIMeasurement] Deactivating HipDI measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateHipDIMeasurementTool(viewportId); } /** * 切换髋关节牵引指数测量工具状态 */ export function toggleHipDIMeasurement(viewportId: string): boolean { console.log( `[toggleHipDIMeasurement] Toggling HipDI measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleHipDIMeasurementTool(viewportId); } /** * 清除髋关节牵引指数测量标注 */ export function clearHipDIMeasurements(viewportId: string): boolean { console.log( `[clearHipDIMeasurements] Clearing HipDI measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearHipDIMeasurements(viewportId); } /** * 获取髋关节牵引指数测量结果 */ // eslint-disable-next-line export function getHipDIMeasurements(viewportId: string): any[] { console.log( `[getHipDIMeasurements] Getting HipDI measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getHipDIMeasurements(viewportId); } /** * 检查髋关节牵引指数测量工具是否激活 */ export function isHipDIMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isHipDIMeasurementToolActive(viewportId); } // ==================== 髋关节水平角测量相关函数 ==================== /** * 激活髋关节水平角测量工具 */ export function activateHipNHAAngleMeasurement(viewportId: string): boolean { console.log( `[activateHipNHAAngleMeasurement] Activating HipNHA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateHipNHAAngleMeasurementTool(viewportId); } /** * 停用髋关节水平角测量工具 */ export function deactivateHipNHAAngleMeasurement(viewportId: string): boolean { console.log( `[deactivateHipNHAAngleMeasurement] Deactivating HipNHA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateHipNHAAngleMeasurementTool(viewportId); } /** * 切换髋关节水平角测量工具状态 */ export function toggleHipNHAAngleMeasurement(viewportId: string): boolean { console.log( `[toggleHipNHAAngleMeasurement] Toggling HipNHA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleHipNHAAngleMeasurementTool(viewportId); } /** * 清除髋关节水平角测量标注 */ export function clearHipNHAAngleMeasurements(viewportId: string): boolean { console.log( `[clearHipNHAAngleMeasurements] Clearing HipNHA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearHipNHAAngleMeasurements(viewportId); } /** * 获取髋关节水平角测量结果 */ // eslint-disable-next-line export function getHipNHAAngleMeasurements(viewportId: string): any[] { console.log( `[getHipNHAAngleMeasurements] Getting HipNHA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getHipNHAAngleMeasurements(viewportId); } /** * 检查髋关节水平角测量工具是否激活 */ export function isHipNHAAngleMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isHipNHAAngleMeasurementToolActive(viewportId); } // ==================== TPLO测量相关函数 ==================== /** * 激活TPLO测量工具 */ export function activateTPLOMeasurement(viewportId: string): boolean { console.log( `[activateTPLOMeasurement] Activating TPLO measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateTPLOMeasurementTool(viewportId); } /** * 停用TPLO测量工具 */ export function deactivateTPLOMeasurement(viewportId: string): boolean { console.log( `[deactivateTPLOMeasurement] Deactivating TPLO measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateTPLOMeasurementTool(viewportId); } /** * 切换TPLO测量工具状态 */ export function toggleTPLOMeasurement(viewportId: string): boolean { console.log( `[toggleTPLOMeasurement] Toggling TPLO measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleTPLOMeasurementTool(viewportId); } /** * 清除TPLO测量标注 */ export function clearTPLOMeasurements(viewportId: string): boolean { console.log( `[clearTPLOMeasurements] Clearing TPLO measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearTPLOMeasurements(viewportId); } /** * 获取TPLO测量结果 */ // eslint-disable-next-line export function getTPLOMeasurements(viewportId: string): any[] { console.log( `[getTPLOMeasurements] Getting TPLO measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getTPLOMeasurements(viewportId); } /** * 检查TPLO测量工具是否激活 */ export function isTPLOMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isTPLOMeasurementToolActive(viewportId); } // ==================== TTA测量相关函数 ==================== /** * 激活TTA测量工具 */ export function activateTTAMeasurement(viewportId: string): boolean { console.log( `[activateTTAMeasurement] Activating TTA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.activateTTAMeasurementTool(viewportId); } /** * 停用TTA测量工具 */ export function deactivateTTAMeasurement(viewportId: string): boolean { console.log( `[deactivateTTAMeasurement] Deactivating TTA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.deactivateTTAMeasurementTool(viewportId); } /** * 切换TTA测量工具状态 */ export function toggleTTAMeasurement(viewportId: string): boolean { console.log( `[toggleTTAMeasurement] Toggling TTA measurement for viewport: ${viewportId}` ); return MeasurementToolManager.toggleTTAMeasurementTool(viewportId); } /** * 清除TTA测量标注 */ export function clearTTAMeasurements(viewportId: string): boolean { console.log( `[clearTTAMeasurements] Clearing TTA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.clearTTAMeasurements(viewportId); } /** * 获取TTA测量结果 */ // eslint-disable-next-line export function getTTAMeasurements(viewportId: string): any[] { console.log( `[getTTAMeasurements] Getting TTA measurements for viewport: ${viewportId}` ); return MeasurementToolManager.getTTAMeasurements(viewportId); } /** * 检查TTA测量工具是否激活 */ export function isTTAMeasurementActive(viewportId: string): boolean { return MeasurementToolManager.isTTAMeasurementToolActive(viewportId); } // ==================== CBLO测量相关函数 ==================== export function activateCBLOMeasurement(viewportId: string): boolean { console.log(`[activateCBLOMeasurement] Activating CBLO measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateCBLOMeasurementTool(viewportId); } export function deactivateCBLOMeasurement(viewportId: string): boolean { console.log(`[deactivateCBLOMeasurement] Deactivating CBLO measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateCBLOMeasurementTool(viewportId); } export function clearCBLOMeasurements(viewportId: string): boolean { console.log(`[clearCBLOMeasurements] Clearing CBLO measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearCBLOMeasurements(viewportId); } // ==================== HipCoverage测量相关函数 ==================== export function activateHipCoverageMeasurement(viewportId: string): boolean { console.log(`[activateHipCoverageMeasurement] Activating HipCoverage measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateHipCoverageMeasurementTool(viewportId); } export function deactivateHipCoverageMeasurement(viewportId: string): boolean { console.log(`[deactivateHipCoverageMeasurement] Deactivating HipCoverage measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateHipCoverageMeasurementTool(viewportId); } export function clearHipCoverageMeasurements(viewportId: string): boolean { console.log(`[clearHipCoverageMeasurements] Clearing HipCoverage measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearHipCoverageMeasurements(viewportId); } // ==================== HipDorsalCoverage测量相关函数 ==================== export function activateHipDorsalCoverageMeasurement(viewportId: string): boolean { console.log(`[activateHipDorsalCoverageMeasurement] Activating HipDorsalCoverage measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateHipDorsalCoverageTool(viewportId); } export function deactivateHipDorsalCoverageMeasurement(viewportId: string): boolean { console.log(`[deactivateHipDorsalCoverageMeasurement] Deactivating HipDorsalCoverage measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateHipDorsalCoverageTool(viewportId); } export function clearHipDorsalCoverageMeasurements(viewportId: string): boolean { console.log(`[clearHipDorsalCoverageMeasurements] Clearing HipDorsalCoverage measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearHipDorsalCoverageMeasurements(viewportId); } // ==================== 找圆心测量相关函数 ==================== export function activateCircleCenterMeasurement(viewportId: string): boolean { console.log(`[activateCircleCenterMeasurement] Activating CircleCenter measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateCircleCenterMeasurementTool(viewportId); } export function deactivateCircleCenterMeasurement(viewportId: string): boolean { console.log(`[deactivateCircleCenterMeasurement] Deactivating CircleCenter measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateCircleCenterMeasurementTool(viewportId); } export function clearCircleCenterMeasurements(viewportId: string): boolean { console.log(`[clearCircleCenterMeasurements] Clearing CircleCenter measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearCircleCenterMeasurements(viewportId); } // ==================== 找中线测量相关函数 ==================== export function activateMidlineMeasurement(viewportId: string): boolean { console.log(`[activateMidlineMeasurement] Activating Midline measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateMidlineMeasurementTool(viewportId); } export function deactivateMidlineMeasurement(viewportId: string): boolean { console.log(`[deactivateMidlineMeasurement] Deactivating Midline measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateMidlineMeasurementTool(viewportId); } export function clearMidlineMeasurements(viewportId: string): boolean { console.log(`[clearMidlineMeasurements] Clearing Midline measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearMidlineMeasurements(viewportId); } // ==================== 找中点测量相关函数 ==================== export function activateFindMidpointMeasurement(viewportId: string): boolean { console.log(`[activateFindMidpointMeasurement] Activating FindMidpoint measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateFindMidpointMeasurementTool(viewportId); } export function deactivateFindMidpointMeasurement(viewportId: string): boolean { console.log(`[deactivateFindMidpointMeasurement] Deactivating FindMidpoint measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateFindMidpointMeasurementTool(viewportId); } export function clearFindMidpointMeasurements(viewportId: string): boolean { console.log(`[clearFindMidpointMeasurements] Clearing FindMidpoint measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearFindMidpointMeasurements(viewportId); } // ==================== 直线垂直倾斜度测量相关函数 ==================== export function activateVerticalTiltMeasurement(viewportId: string): boolean { console.log(`[activateVerticalTiltMeasurement] Activating VerticalTilt measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateVerticalTiltMeasurementTool(viewportId); } export function deactivateVerticalTiltMeasurement(viewportId: string): boolean { console.log(`[deactivateVerticalTiltMeasurement] Deactivating VerticalTilt measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateVerticalTiltMeasurementTool(viewportId); } export function clearVerticalTiltMeasurements(viewportId: string): boolean { console.log(`[clearVerticalTiltMeasurements] Clearing VerticalTilt measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearVerticalTiltMeasurements(viewportId); } // ==================== 直线水平倾斜度测量相关函数 ==================== export function activateHorizontalTiltMeasurement(viewportId: string): boolean { console.log(`[activateHorizontalTiltMeasurement] Activating HorizontalTilt measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateHorizontalTiltMeasurementTool(viewportId); } export function deactivateHorizontalTiltMeasurement(viewportId: string): boolean { console.log(`[deactivateHorizontalTiltMeasurement] Deactivating HorizontalTilt measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateHorizontalTiltMeasurementTool(viewportId); } export function clearHorizontalTiltMeasurements(viewportId: string): boolean { console.log(`[clearHorizontalTiltMeasurements] Clearing HorizontalTilt measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearHorizontalTiltMeasurements(viewportId); } // ==================== 多边形长度测量相关函数 ==================== /** * 激活多边形长度测量工具 */ export function activatePolygonLengthMeasurement(viewportId: string): boolean { console.log(`[activatePolygonLengthMeasurement] Activating PolygonLength measurement for viewport: ${viewportId}`); return MeasurementToolManager.activatePolygonLengthMeasurementTool(viewportId); } /** * 停用多边形长度测量工具 */ export function deactivatePolygonLengthMeasurement(viewportId: string): boolean { console.log(`[deactivatePolygonLengthMeasurement] Deactivating PolygonLength measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivatePolygonLengthMeasurementTool(viewportId); } /** * 清除多边形长度测量标注 */ export function clearPolygonLengthMeasurements(viewportId: string): boolean { console.log(`[clearPolygonLengthMeasurements] Clearing PolygonLength measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearPolygonLengthMeasurements(viewportId); } // ==================== 拆线长度测量相关函数 ==================== /** * 激活拆线长度测量工具 */ export function activatePolylineLengthMeasurement(viewportId: string): boolean { console.log(`[activatePolylineLengthMeasurement] Activating PolylineLength measurement for viewport: ${viewportId}`); return MeasurementToolManager.activatePolylineLengthMeasurementTool(viewportId); } /** * 停用拆线长度测量工具 */ export function deactivatePolylineLengthMeasurement(viewportId: string): boolean { console.log(`[deactivatePolylineLengthMeasurement] Deactivating PolylineLength measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivatePolylineLengthMeasurementTool(viewportId); } /** * 清除拆线长度测量标注 */ export function clearPolylineLengthMeasurements(viewportId: string): boolean { console.log(`[clearPolylineLengthMeasurements] Clearing PolylineLength measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearPolylineLengthMeasurements(viewportId); } // ==================== 直线灰度测量相关函数 ==================== /** * 激活直线灰度测量工具 */ export function activateLineGrayscaleMeasurement(viewportId: string): boolean { console.log(`[activateLineGrayscaleMeasurement] Activating LineGrayscale measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateLineGrayscaleMeasurementTool(viewportId); } /** * 停用直线灰度测量工具 */ export function deactivateLineGrayscaleMeasurement(viewportId: string): boolean { console.log(`[deactivateLineGrayscaleMeasurement] Deactivating LineGrayscale measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateLineGrayscaleMeasurementTool(viewportId); } /** * 清除直线灰度测量标注 */ export function clearLineGrayscaleMeasurements(viewportId: string): boolean { console.log(`[clearLineGrayscaleMeasurements] Clearing LineGrayscale measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearLineGrayscaleMeasurements(viewportId); } // ==================== 矩形区域灰度测量相关函数 ==================== /** * 激活矩形区域灰度测量工具 */ export function activateRectangleGrayscaleMeasurement(viewportId: string): boolean { console.log(`[activateRectangleGrayscaleMeasurement] Activating RectangleGrayscale measurement for viewport: ${viewportId}`); return MeasurementToolManager.activateRectangleGrayscaleMeasurementTool(viewportId); } /** * 停用矩形区域灰度测量工具 */ export function deactivateRectangleGrayscaleMeasurement(viewportId: string): boolean { console.log(`[deactivateRectangleGrayscaleMeasurement] Deactivating RectangleGrayscale measurement for viewport: ${viewportId}`); return MeasurementToolManager.deactivateRectangleGrayscaleMeasurementTool(viewportId); } /** * 清除矩形区域灰度测量标注 */ export function clearRectangleGrayscaleMeasurements(viewportId: string): boolean { console.log(`[clearRectangleGrayscaleMeasurements] Clearing RectangleGrayscale measurements for viewport: ${viewportId}`); return MeasurementToolManager.clearRectangleGrayscaleMeasurements(viewportId); } // ==================== 掩码工具相关函数 ==================== /** * 激活掩码工具 */ export function activateMask(viewportId: string): boolean { console.log(`[activateMask] Activating mask tool for viewport: ${viewportId}`); return MeasurementToolManager.activateMaskTool(viewportId); } /** * 停用掩码工具 */ export function deactivateMask(viewportId: string): boolean { console.log(`[deactivateMask] Deactivating mask tool for viewport: ${viewportId}`); return MeasurementToolManager.deactivateMaskTool(viewportId); } /** * 清除掩码标注 */ export function clearMask(viewportId: string): boolean { console.log(`[clearMask] Clearing mask annotations for viewport: ${viewportId}`); return MeasurementToolManager.clearMaskAnnotations(viewportId); } export class ImageLoadError extends Error { constructor(message: string, failedImageIds: string[]) { super(message); this.name = 'ImageLoadError'; this.failedImageIds = failedImageIds; // 附加失败 ID 列表 } failedImageIds: string[]; } /** * 安全的图像栈设置函数,带超时保护和错误捕获 * @param viewport - Cornerstone viewport 实例 * @param imageIds - 图像 ID 数组 * @param imageIndex - 当前图像索引 * @param timeout - 超时时间(毫秒),默认30秒 */ export async function safeSetStack( viewport: any, imageIds: string[], imageIndex: number, timeout: number = 30000 ): Promise { const errors: string[] = []; // 错误事件处理器 const handler = (evt: any) => { console.error(`捕获到图像加载错误: ${evt.detail.imageId}`); errors.push(evt.detail.imageId); }; // 添加错误监听器 eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, handler); try { // 创建超时 Promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`图像加载超时 (${timeout}ms)`)); }, timeout); }); // 等待 setStack 完成或超时 await Promise.race([ viewport.setStack(imageIds, imageIndex), timeoutPromise ]); // setStack 完成后,检查是否有错误 if (errors.length > 0) { throw new ImageLoadError( `图像加载失败:共 ${errors.length} 张(${errors.join(', ')})`, errors ); } console.log(`✅ 图像栈设置成功: ${imageIds.length} 张图像`); } finally { // 无论成功失败,都移除监听器(清理资源) eventTarget.removeEventListener(EVENTS.IMAGE_LOAD_ERROR, handler); } } const StackViewer = ({ imageIndex = 0, imageUrls = [], viewportId, renderingEngineId, selected }: { imageIndex?: number; imageUrls?: string[]; viewportId: string; renderingEngineId: string; selected?: boolean; }) => { const elementRef = useRef(null); // 用于捕获异步错误并在渲染时抛出,让 Error Boundary 能够捕获 const [renderError, setRenderError] = React.useState(null); // 获取overlay启用状态 const overlayEnabled = useSelector(selectOverlayEnabled); // 获取 dispatch const dispatch = useDispatch(); // 监听overlay状态变化,启用/禁用DicomOverlayTool useEffect(() => { try { const toolGroup = MeasurementToolManager.getToolGroup(viewportId); // const toolGroup = ToolGroupManager.getToolGroup(`STACK_TOOL_GROUP_ID_${viewportId}`); if (!toolGroup) { console.warn(`[StackViewer] Tool group not found for viewport: ${viewportId}`); return; } if (overlayEnabled) { console.log(`[StackViewer] Enabling DicomOverlay for viewport: ${viewportId}`); toolGroup.setToolEnabled(DicomOverlayTool.toolName); } else { console.log(`[StackViewer] Disabling DicomOverlay for viewport: ${viewportId}`); toolGroup.setToolDisabled(DicomOverlayTool.toolName); //再次启用,表示关闭四角信息 toolGroup.setToolEnabled(DicomOverlayTool.toolName); } } catch (error) { console.error(`[StackViewer] Error toggling DicomOverlay:`, error); } }, [overlayEnabled, viewportId]); // // 监听imageUrls变化并重新加载图像 // useEffect(() => { // const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId); // if (!renderingEngine) { // return; // } // const viewport = renderingEngine.getViewport(viewportId) as cornerstone.Types.IStackViewport; // if (viewport && imageUrls.length > 0) { // console.log(`[StackViewer] imageUrls changed for viewport ${viewportId}, reloading...`); // viewport.setStack(imageUrls, imageIndex).then(() => { // viewport.render(); // }).catch((error) => { // console.error('[StackViewer] Error reloading image stack:', error); // }); // } // }, [imageUrls, imageIndex, viewportId, renderingEngineId]); useEffect(() => { const setup = async () => { // 清除之前的错误状态(如果有) setRenderError(null); // // 初始化 Cornerstone // cornerstone.init(); // cornerstoneTools.init(); // const state = store.getState(); // console.log(`当前系统模式:${state.systemMode.mode}`); // const token = // state.systemMode.mode === SystemMode.Emergency // ? state.product.guest // : state.userInfo.token; // console.log(`token stack.image.viewer: ${token}`); // cornerstoneDICOMImageLoader.init({ // maxWebWorkers: navigator.hardwareConcurrency || 1, // errorInterceptor: (error) => { // if (error.status === 401) { // console.error('Authentication failed. Please refresh the token.'); // } // console.error(`请求dcm文件出错:${error}`); // }, // beforeSend: (xhr, imageId, defaultHeaders) => { // return { // ...defaultHeaders, // Authorization: `Bearer ${token}`, // Language: 'en', // Product: 'DROS', // Source: 'Electron', // }; // }, // }); // const currentViewportId = viewportId; eventTarget.addEventListener( cornerstoneTools.Enums.Events.ANNOTATION_COMPLETED, (evt) => { const { annotation } = evt.detail; console.log('Annotation completed event:', annotation); if (annotation.metadata.toolName === 'LinearSplineROI') { console.log('SplineROITool annotation completed:', annotation); overlayRedRectangle(viewportId); //取消工具激活状态 const toolGroup = ToolGroupManager.getToolGroup( 'STACK_TOOL_GROUP_ID' ); if (!toolGroup) { console.log('toolGroup not found'); } toolGroup?.setToolPassive('LinearSplineROI', { removeAllBindings: true, }); } } ); const viewportInput: cornerstone.Types.PublicViewportInput = { viewportId, element: elementRef.current!, type: cornerstone.Enums.ViewportType.STACK, }; const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId); if (!renderingEngine) { console.error( `[stack.image.viewer] No rendering engine with id ${renderingEngineId} found` ); return; } // Enable the element for use with Cornerstone renderingEngine.enableElement(viewportInput); registerTools(viewportId, renderingEngine.id); // Get the stack viewport that was created const viewport = renderingEngine.getViewport( viewportId ) as cornerstone.Types.IStackViewport; // 增强的图像加载逻辑,包含完善的错误处理 try { console.log(`重新加载图像----开始`); console.log(`图像URLs:`, imageUrls); console.log(`图像索引:`, imageIndex); // 1. 验证图像URLs if (!imageUrls || imageUrls.length === 0) { throw new Error('图像URL列表为空'); } // 2. 验证图像索引 if (imageIndex < 0 || imageIndex >= imageUrls.length) { throw new Error(`图像索引无效: ${imageIndex}, 有效范围: 0-${imageUrls.length - 1}`); } // 3. 预检查图像可访问性(可选) // await validateImageUrls(imageUrls); // 4. 多帧DICOM预处理:检测并扩展imageIds let finalImageUrls = imageUrls; let detectedFrameCount = 1; // 只有当imageUrls只有一个元素时,才可能是多帧DICOM需要expansion if (imageUrls.length === 1) { try { // 预先分析DICOM元数据以检测帧数 const { DicomMetadataAnalyzer } = await import('@/utils/dicom/DicomMetadataAnalyzer'); const quickAnalysis = await DicomMetadataAnalyzer.analyze(imageUrls[0], undefined, { baseUrl: getIpPort() }); detectedFrameCount = quickAnalysis.frameCount; // 如果检测到多帧,扩展imageIds数组 if (detectedFrameCount > 1) { console.log(`[StackViewer] 检测到多帧DICOM (${detectedFrameCount}帧),扩展imageIds...`); const baseImageId = imageUrls[0]; const expandedImageIds: string[] = []; for (let i = 0; i < detectedFrameCount; i++) { // 为每一帧创建对应的imageId expandedImageIds.push(`${baseImageId}${baseImageId.includes('?') ? '&' : '?'}frame=${i}`); } finalImageUrls = expandedImageIds; console.log(`[StackViewer] ImageIds已扩展:`, { 原始: 1, 扩展后: expandedImageIds.length, 示例: expandedImageIds.slice(0, Math.min(3, expandedImageIds.length)) }); } } catch (error) { console.warn(`[StackViewer] 多帧预检测失败,使用原始imageUrls:`, error); } } // 5. 设置图像栈,使用扩展后的imageUrls await safeSetStack(viewport, finalImageUrls, imageIndex); // 5. 初始化播放状态(用于多帧播放控制) // 使用 DicomMetadataAnalyzer 进行深度分析,从 DICOM 元数据读取帧数 let totalFrames: number; // Declare totalFrames here try { // 导入 DicomMetadataAnalyzer (需要在文件顶部添加 import) const { DicomMetadataAnalyzer } = await import('@/utils/dicom/DicomMetadataAnalyzer'); // 使用第一个图像进行分析 const imageId = imageUrls[imageIndex]; // 使用 DicomMetadataAnalyzer 进行完整分析 const analysisResult = await DicomMetadataAnalyzer.analyze(imageId, viewport, { baseUrl: getIpPort() }); // 获取 viewport 的实际 imageIds(用于交叉验证) const actualImageIds = viewport.getImageIds(); const viewportFrameCount = actualImageIds.length; // 混合验证:比较元数据帧数与 viewport 帧数 const metadataFrameCount = analysisResult.frameCount; totalFrames = metadataFrameCount;//viewportFrameCount; // Assign value here // 优先使用 viewport 的值,它反映实际加载情况 // 验证一致性 const isConsistent = metadataFrameCount === viewportFrameCount; if (!isConsistent) { console.warn(`[StackViewer] 帧数不一致检测:`, { imageId, metadataFrames: metadataFrameCount, viewportFrames: viewportFrameCount, analysisConfidence: analysisResult.confidence }); } console.log(`[StackViewer] DICOM分析完成:`, { imageUrlsCount: imageUrls.length, metadataFrames: metadataFrameCount, viewportFrames: viewportFrameCount, finalTotalFrames: totalFrames, isMultiFrame: totalFrames > 1, frameType: analysisResult.frameType, isEnhanced: analysisResult.isEnhanced, confidence: analysisResult.confidence, isConsistent }); dispatch(initPlayback({ viewportId, totalFrames })); // 标记帧检测完成,使用完整的分析结果 dispatch(setFrameDetectionResult({ viewportId, isMultiFrame: totalFrames > 1, totalFrames, analysisResult: { ...analysisResult, // 使用 viewport 的实际帧数覆盖分析结果 frameCount: totalFrames, isMultiFrame: totalFrames > 1 } })); } catch (error) { // 如果 DicomMetadataAnalyzer 失败,降级到简单检测 console.warn(`[StackViewer] DICOM分析失败,降级到简单检测:`, error); const actualImageIds = viewport.getImageIds(); totalFrames = actualImageIds.length; // Assign value here console.log(`[StackViewer] 使用简单检测方法:`, { imageUrlsCount: imageUrls.length, actualFrames: totalFrames, isMultiFrame: totalFrames > 1 }); dispatch(initPlayback({ viewportId, totalFrames })); dispatch(setFrameDetectionResult({ viewportId, isMultiFrame: totalFrames > 1, totalFrames, analysisResult: { frameCount: totalFrames, isMultiFrame: totalFrames > 1, frameType: 'dynamic', isEnhanced: false, analysisTimestamp: new Date(), analysisVersion: '1.0.0', confidence: 0.5 // 降级检测的可信度较低 } })); } console.log(`[StackViewer] 播放状态初始化完成: ${totalFrames} 帧`); // This log statement is now outside the inner try-catch // 6. 图像加载完成后,自动应用保持宽高比的适应 fitImageWithAspectRatio(viewportId); console.log(`重新加载图像----结束`); } catch (error) { // 不直接抛出错误,而是保存到状态中 // 在下次渲染时抛出,这样 Error Boundary 才能捕获 const enhancedError = enhanceError(error, { imageUrls, imageIndex, viewportId }); setRenderError(enhancedError); return; // 不再继续执行后续代码 // 根据错误严重程度决定处理策略 if (isCriticalError(enhancedError)) { // 严重错误:保存到状态,让 Error Boundary 处理 console.error('🚨 严重图像加载错误,将在渲染时抛出给 Error Boundary:', enhancedError); setRenderError(enhancedError); return; // 不再继续执行后续代码 } else { // 非严重错误:仅记录日志,继续运行 console.warn('⚠️ 图像加载警告:', enhancedError); // 可以在这里尝试降级策略,比如显示占位符 } } viewport.render(); }; setup(); // ✅ 添加清理函数:确保组件卸载时正确清理 viewport return () => { try { const renderingEngine = cornerstone.getRenderingEngine(renderingEngineId); if (renderingEngine) { console.log(`[StackViewer] Cleaning up viewport: ${viewportId}`); renderingEngine.disableElement(viewportId); } } catch (error) { console.warn(`[StackViewer] Error disabling element ${viewportId}:`, error); } }; }, [elementRef, imageIndex, viewportId, renderingEngineId, imageUrls[0]]); // 监听容器大小变化,自动保持宽高比 useEffect(() => { if (!elementRef.current) return; // 创建防抖的适应函数 const debouncedFit = debounce(() => { // console.log(`容器大小变化了---------`) requestAnimationFrame(() => { fitImageWithAspectRatio(viewportId); }); }, 100); // 创建ResizeObserver监听容器大小变化 const resizeObserver = new ResizeObserver(debouncedFit); resizeObserver.observe(elementRef.current); console.log(`[StackViewer] ResizeObserver attached to viewport: ${viewportId}`); // 清理函数 return () => { resizeObserver.disconnect(); console.log(`[StackViewer] ResizeObserver disconnected from viewport: ${viewportId}`); }; }, [viewportId]); // 在渲染期间检查错误状态,如果有错误则抛出 // 这样 Error Boundary 就能捕获到异步错误了 if (renderError) { throw renderError; } return (
e.preventDefault()} style={{ width: '100%', height: '100%', backgroundColor: '#000', border: selected ? '2px solid blue' : '1px solid gray', position: 'relative', // 让浮动控制条能够正确定位 }} > {/* 播放控制器(逻辑组件,不渲染UI) */} {/* 浮动播放控制条(UI组件) */}
); }; /** * 带错误边界的 StackViewer 包装组件 * 自动处理图像加载错误,提供优雅的错误恢复机制 */ export const StackViewerWithErrorBoundary = ({ imageIndex = 0, imageUrls = [], viewportId, renderingEngineId, selected, maxRetries = 3, onError }: { imageIndex?: number; imageUrls?: string[]; viewportId: string; renderingEngineId: string; selected?: boolean; maxRetries?: number; onError?: (error: Error, errorInfo: any) => void; }) => { return ( ); }; export default StackViewer;