OHIF 3.10 到 3.11 迁移指南:基于promptHydrationDialog与hydrateSecondaryDisplaySet命令的新版 Hydration 体系
【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers
导读
本文面向从 OHIF 3.10 升级到 3.11 的开发者,聚焦本版本中最重要的架构变化之一——Hydration(水合)机制的重构:原先分散在各扩展中、各自维护一套 Promise 与对话框逻辑的promptHydrateSEG/promptHydrateRT提示函数,被统一收敛到@ohif/extension-cornerstone的通用工具utils.promptHydrationDialog;而真正执行水合的动作,则改为由hydrateSecondaryDisplaySet命令统一驱动。读完本文,你将掌握 3.11 中 SEG、RTSTRUCT、SR 三类二次显示集(secondary display set)水合提示与执行的新调用链、对话框可定制机制、以及如何把存量 3.10 代码迁移到新 API。
背景:3.11 的 Hydration 体系为什么重构
在 3.10 及更早版本中,SEG(分割)与 RTSTRUCT(放疗结构)的水合提示逻辑存在大量重复代码:每个扩展(cornerstone-dicom-seg、cornerstone-dicom-rt)各自定义了一套RESPONSE常量、各自实现_askHydrate对话框 Promise、各自处理视口交互与回调。这不仅造成代码重复,也让"是否弹窗、弹什么文案、确认后执行什么"这些行为难以在应用层统一定制。
3.11 的改动方向清晰可见:
- 对话框逻辑下沉为通用工具:
promptHydrateSEG与promptHydrateRT改为薄封装,内部统一委托给@ohif/extension-cornerstone导出的utils.promptHydrationDialog(参见 promptHydrationDialog.ts); - 执行动作命令化:真正的水合动作不再由各扩展自行编写回调闭包,而是普遍通过
hydrateSecondaryDisplaySet命令触发(参见 commandsModule.ts)。
这一设计让"提示"与"执行"解耦:提示层只负责询问用户并返回决策,执行层以命令形式注册、可被任意调用方复用。
通用提示工具:utils.promptHydrationDialog
3.11 将对话框提示逻辑统一封装在promptHydrationDialog中,并作为@ohif/extension-cornerstone的utils导出(见 utils/index.ts 与 utils/promptHydrationDialog.ts)。
签名与参数
export type HydrationCallback = (params: any) => Promise<boolean>; export interface HydrationDialogProps { servicesManager: AppTypes.ServicesManager; viewportId: string; displaySet: AppTypes.DisplaySet; preHydrateCallbacks?: HydrationCallback[]; // 可选,默认 [] hydrateCallback: HydrationCallback; // 用户确认后执行的回调 type: string; // 'SEG' | 'SR' | 'RTSTRUCT' } function promptHydrationDialog({ servicesManager, viewportId, displaySet, preHydrateCallbacks = [], hydrateCallback, type, }: HydrationDialogProps): Promise<boolean | HydrationSRResult>各参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
servicesManager | AppTypes.ServicesManager | OHIF 服务管理器,内部用于取出uiViewportDialogService、customizationService以及_extensionManager上的appConfig |
viewportId | string | 发起水合提示的视口 ID,对话框将挂载到该视口上 |
displaySet | AppTypes.DisplaySet | 待水合的二次显示集(SEG / RTSTRUCT / SR display set) |
preHydrateCallbacks | HydrationCallback[](可选) | 用户在对话框中确认后、执行hydrateCallback之前依次执行的钩子(如保存当前呈现状态 presentation state) |
hydrateCallback | HydrationCallback | 用户确认后真正执行水合的回调,返回Promise<boolean> |
type | string | 水合类型,取自HydrationType枚举:SEG、SR、RTSTRUCT |
返回值是一个 Promise:非 SR 类型在用户确认并完成水合后解析为boolean(成功为true),用户取消则解析为false;SR 类型则返回结构化的HydrationSRResult(详见下文"SR 的特殊处理")。
类型常量与响应码
promptHydrationDialog内部定义了两组关键常量:
export const HydrationType = { SEG: 'SEG', SR: 'SR', RTSTRUCT: 'RTSTRUCT', } as const; const RESPONSE = { NO_NEVER: -1, CANCEL: 0, CREATE_REPORT: 1, ADD_SERIES: 2, SET_STUDY_AND_SERIES: 3, NO_NOT_FOR_SERIES: 4, HYDRATE: 5, };其中RESPONSE.CANCEL(用户点"No"或点击对话框外部)与RESPONSE.HYDRATE(用户点"Yes"或按 Enter)是 RT/SEG 流程的两个关键分支:只有HYDRATE才会继续执行preHydrateCallbacks与hydrateCallback。
是否弹窗的判定逻辑
这是 3.11 中最值得注意的行为变化之一。promptHydrationDialog通过appConfig决定是否真正弹出确认框:
const shouldPrompt = type === HydrationType.SR ? standardMode : !appConfig?.disableConfirmationPrompts;- RTSTRUCT / SEG:读取
appConfig.disableConfirmationPrompts。为true时跳过对话框、直接视为用户确认(RESPONSE.HYDRATE),实现"无感水合"; - SR:不看
disableConfirmationPrompts,而是检查appConfig.measurementTrackingMode === 'standard'(即"标准测量跟踪模式"),只有在 standard 模式下才弹窗询问。
从源码注释(promptHydrationDialog.ts)可以看出这是刻意设计:RT/SEG 关心"是否关闭确认提示",SR 关心"是否处于标准测量跟踪模式"。这一分支逻辑在 promptHydrationDialog.test.ts 中有大量针对性测试覆盖,例如"disableConfirmationPrompts为 true 时非 SR 类型应直接返回 HYDRATE 响应"。
对话框内容与交互的可定制性
_askHydrate负责真正的对话框交互:
- 文案可定制:消息内容通过
customizationService.getCustomization(messageKey)获取,按类型映射到不同的 customization key:
switch (type) { case HydrationType.RTSTRUCT: return 'viewportNotification.hydrateRTMessage'; case HydrationType.SEG: return 'viewportNotification.hydrateSEGMessage'; case HydrationType.SR: return 'viewportNotification.hydrateSRMessage'; default: return 'viewportNotification.hydrateMessage'; }这意味着应用可以通过 OHIF 的 customization 机制覆盖这些 key,自定义弹窗文案而无需改扩展源码。
交互方式:对话框通过
uiViewportDialogService.show(...)展示,提供 "No"(secondary样式,值CANCEL)与 "Yes"(primary样式,值HYDRATE)两个动作按钮;点击外部区域等同于取消;按 Enter 键等同于确认。SEG 与 RT 的执行差异:用户确认后,SEG 与 RTSTRUCT 均在
window.setTimeout(..., 0)中异步执行hydrateCallback,以让出主线程;区别在于回调入参——SEG 收到{ segDisplaySet, viewportId },RTSTRUCT 收到{ rtDisplaySet, viewportId, servicesManager }。
SR 的特殊处理
SR 类型不走简单的 boolean 返回,而是返回HydrationSRResult:
export interface HydrationSRResult { userResponse: number; displaySetInstanceUID: string; srSeriesInstanceUID: string; viewportId: string; StudyInstanceUID?: string; SeriesInstanceUIDs?: string[]; }用户确认后,hydrateCallback(displaySet)的返回值中若包含StudyInstanceUID/SeriesInstanceUIDs,会一并并入结果,供上层(如测量跟踪上下文)继续使用;用户取消时同样返回带userResponse的结果对象而非false。
迁移实操:promptHydrateRT与promptHydrateSEG的改造
原文档给出的迁移路径非常具体。以 RT 为例,改造前后对照如下。
promptHydrateRT:删除旧对话框逻辑,改为薄封装
原文档展示的extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts变化:
- const RESPONSE = { - NO_NEVER: -1, - CANCEL: 0, - HYDRATE_SEG: 5, - }; + import { utils, Types } from '@ohif/extension-cornerstone'; function promptHydrateRT({ servicesManager, rtDisplaySet, viewportId, preHydrateCallbacks, hydrateRTDisplaySet, -}: withAppTypes) { - const { uiViewportDialogService, customizationService } = servicesManager.services; - // ... lots of old promise and dialog logic - return new Promise(async function (resolve, reject) { - // ... - }); -} - -function _askHydrate( - // ... -) { - // ... -} +}: { + servicesManager: AppTypes.ServicesManager; + rtDisplaySet: AppTypes.DisplaySet; + viewportId: string; + preHydrateCallbacks?: Types.HydrationCallback[]; + hydrateRTDisplaySet: Types.HydrationCallback; +}) { + return utils.promptHydrationDialog({ + servicesManager, + viewportId, + displaySet: rtDisplaySet, + preHydrateCallbacks, + hydrateCallback: hydrateRTDisplaySet, + type: 'RTSTRUCT', + }); }对照当前仓库的实际实现(extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts),最终形态与 diff 中的新代码完全一致:整个函数只剩 10 行左右的委托逻辑,旧的RESPONSE常量与_askHydrate已彻底删除。promptHydrateSEG的改造同理(见 extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts),只是type传'SEG'、入参名为segDisplaySet与hydrateCallback。
调用方:Viewport 内改由命令触发水合
原文档展示的extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx变化:
useEffect(() => { if (rtIsLoading) { return; } promptHydrateRT({ servicesManager, viewportId, rtDisplaySet, - preHydrateCallbacks: [storePresentationState], - hydrateRTDisplaySet, - }).then(isHydrated => { - if (isHydrated) { - setIsHydrated(true); - } + hydrateRTDisplaySet: async () => { + return commandsManager.runCommand('hydrateSecondaryDisplaySet', { + displaySet: rtDisplaySet, + viewportId, + }); + }, }); - }, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]); + }, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager]);对照当前实现(OHIFCornerstoneRTViewport.tsx),可以看到两个要点:
- 回调交给命令:
hydrateRTDisplaySet不再指向旧的自定义水合函数,而是内联为commandsManager.runCommand('hydrateSecondaryDisplaySet', { displaySet: rtDisplaySet, viewportId }); - 依赖数组补全:
useEffect的依赖数组加入了commandsManager(当前实现还额外加入了activeViewportId,且增加了"非活动视口直接返回"的守卫判断,避免后台视口误触发水合提示)。
SEG 侧的实现对称(OHIFCornerstoneSEGViewport.tsx):promptHydrateSEG的hydrateCallback同样调用hydrateSecondaryDisplaySet命令,并显式返回true。
SR 的对应改造
SR 场景位于测量跟踪扩展中(promptHydrateStructuredReport.ts):它通过事件参数中的viewportId与displaySetInstanceUID定位显示集,构造hydrateCallback调用hydrateSecondaryDisplaySet命令,然后委托给utils.promptHydrationDialog并传type: 'SR'。注意它额外做了enhancedSrDisplaySet的浅拷贝以补全displaySetInstanceUID字段,说明 SR 调用方需要为对话框提供完整的显示集标识。
执行核心:hydrateSecondaryDisplaySet命令
hydrateSecondaryDisplaySet注册于@ohif/extension-cornerstone的 commandsModule(commandsModule.ts),签名与核心逻辑如下:
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => { if (!displaySet) { return; } const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); if (displaySet.isOverlayDisplaySet) { // 根据模态与视口类型推断分割表示类型 const segmentationType = displaySet.Modality !== 'SEG' ? SegmentationRepresentations.Contour : viewport && isVolume3DViewportType(viewport) ? SegmentationRepresentations.Surface : SegmentationRepresentations.Labelmap; commandsManager.runCommand('updateStoredSegmentationPresentation', { displaySet, type: segmentationType, }); } // isHydrated 表示"该显示集作为标准视图的一部分展示", // 该判定在这里完成,不依赖视口是否已存在 displaySet.isHydrated = true; const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID; // ... };结合 commandsModule.ts 的后续分支,该命令的核心职责可以归纳为:
- 空参守卫:
displaySet不存在时直接返回; - Overlay 显示集的表示类型推断:对于
isOverlayDisplaySet(派生覆盖显示集),根据Modality与视口类型决定分割表示——非 SEG(如 RTSTRUCT)用Contour(轮廓),SEG 在 3D 体积视口用Surface(曲面),否则用Labelmap(标签图),并调用updateStoredSegmentationPresentation更新存储的分割呈现; - 标记
isHydrated = true:这是命令对显示集状态的直接写入,后续挂片协议与视口布局据此判定"该显示集已是标准视图的一部分"; - 按模态分发:
SEG/RTSTRUCT:先通过storePositionPresentation将引用显示集的"位置呈现"(position/zoom/pan)关联到目标视口,再调用loadSegmentationDisplaySetsForViewport加载分割显示集,并支持读取panelSegmentation.disableEditing定制项以在加载后锁定分割段;SR:调用hydrateStructuredReport命令,并根据返回的SeriesInstanceUIDs解析被引用的显示集继续处理。
此外,命令内部还对水合后的"多视口刷新"做了处理(见 utils/hydrationUtils.ts):SEG 水合后,除挂片协议匹配到的活动视口外,还会按帧参考系(frame of reference)合并所有已展示同一体积的网格视口,确保分割呈现同步应用到 MPR / 3D 瓦片,这体现了 3.11 中"水合 = 在它逻辑上应该出现的地方展示它"的设计意图。
迁移核对清单
在 3.10 → 3.11 升级中完成 Hydration 相关代码迁移,建议按以下清单核对:
- 删除各扩展内自有的
RESPONSE常量与_askHydrate实现,将promptHydrateSEG/promptHydrateRT收敛为对utils.promptHydrationDialog的薄封装; - 将 Viewport 中的水合执行动作替换为
hydrateSecondaryDisplaySet命令,并确认useEffect依赖数组包含commandsManager(以及必要的activeViewportId守卫); - 确认 SR 场景使用
type: 'SR'走promptHydrationDialog的 SR 分支,其返回值是HydrationSRResult而非boolean; - 核对应用配置:
disableConfirmationPrompts决定 RT/SEG 是否弹窗,measurementTrackingMode决定 SR 是否弹窗——若发现弹窗行为与 3.10 不同,优先检查appConfig中的这两项; - 如需定制文案,通过 customization 覆盖
viewportNotification.hydrateRTMessage/hydrateSEGMessage/hydrateSRMessage; - 跑一遍测试:
promptHydrationDialog的判定与回调行为已由 promptHydrationDialog.test.ts 覆盖(包括disableConfirmationPrompts、measurementTrackingMode、SEG 的延迟执行、SR 结果结构等分支),升级后应确保这些用例通过。
小结
3.11 的 Hydration 体系本质上是把"分散的、各写各的"水合提示与执行逻辑,收敛为"promptHydrationDialog统一提示 +hydrateSecondaryDisplaySet统一执行"的两段式架构。对扩展开发者而言,这意味着更少的样板代码、更统一的定制入口,以及更可测试的行为边界——升级时只需按照本文的迁移清单替换旧函数与旧回调,即可无缝接入新体系。
【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考