ViewerContainer.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import React, { useEffect } from 'react';
  2. import StackViewer, {
  3. activateMagnifier,
  4. addLMark,
  5. addMask,
  6. addRLabel,
  7. adjustBrightnessAndContrast,
  8. ApplyColormap,
  9. deleteSelectedMark,
  10. fitImageSize,
  11. HorizontalFlip,
  12. invertContrast,
  13. InvertImage,
  14. remoteMask,
  15. ResetImage,
  16. rotateAnyAngle,
  17. RotateClockwise90,
  18. RotateCounterclockwise90,
  19. setOriginalSize,
  20. VerticalFlip,
  21. activateLengthMeasurement,
  22. deactivateLengthMeasurement,
  23. clearLengthMeasurements,
  24. activateAngleMeasurement,
  25. deactivateAngleMeasurement,
  26. clearAngleMeasurements,
  27. } from './viewers/stack.image.viewer';
  28. import { useSelector, useDispatch } from 'react-redux';
  29. import store, { RootState } from '@/states/store';
  30. import { clearAction } from '@/states/view/functionAreaSlice';
  31. import {
  32. selectGridLayout,
  33. selectSelectedViewers,
  34. selectAllViewerUrls,
  35. setGridLayout as setGridLayoutAction,
  36. setAllViewers,
  37. toggleViewerSelection,
  38. } from '@/states/view/viewerContainerSlice';
  39. import {
  40. clearMeasurementAction,
  41. selectCurrentMeasurementAction,
  42. setToolActive,
  43. setToolInactive,
  44. } from '@/states/view/measurementPanelSlice';
  45. import * as cornerstone from '@cornerstonejs/core';
  46. import * as cornerstoneTools from '@cornerstonejs/tools';
  47. import { SystemMode } from '@/states/systemModeSlice';
  48. import * as cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
  49. import { MeasurementToolManager } from '@/utils/measurementToolManager';
  50. const renderingEngineId = 'myRenderingEngine';
  51. const setup = () => {
  52. // 初始化 Cornerstone
  53. cornerstone.init();
  54. cornerstoneTools.init();
  55. const state = store.getState();
  56. console.log(`当前系统模式:${state.systemMode.mode}`);
  57. const token =
  58. state.systemMode.mode === SystemMode.Emergency
  59. ? state.product.guest
  60. : state.userInfo.token;
  61. console.log(`token stack.image.viewer: ${token}`);
  62. cornerstoneDICOMImageLoader.init({
  63. maxWebWorkers: navigator.hardwareConcurrency || 1,
  64. errorInterceptor: (error) => {
  65. if (error.status === 401) {
  66. console.error('Authentication failed. Please refresh the token.');
  67. }
  68. console.error(`请求dcm文件出错:${error}`);
  69. },
  70. beforeSend: (xhr, imageId, defaultHeaders) => {
  71. return {
  72. ...defaultHeaders,
  73. Authorization: `Bearer ${token}`,
  74. Language: 'en',
  75. Product: 'DROS',
  76. Source: 'Electron',
  77. };
  78. },
  79. });
  80. // 创建渲染引擎
  81. new cornerstone.RenderingEngine(renderingEngineId);
  82. };
  83. setup();
  84. interface ViewerContainerProps {
  85. imageUrls: string[];
  86. }
  87. const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
  88. console.log(`[ViewerContainer] 新渲染 imageUrls:`, imageUrls);
  89. // 从 Redux 获取状态
  90. const gridLayout = useSelector(selectGridLayout);
  91. const selectedViewerUrls = useSelector(selectSelectedViewers);
  92. const allViewerUrls = useSelector(selectAllViewerUrls);
  93. const action = useSelector((state: RootState) => state.functionArea.action);
  94. const measurementAction = useSelector(selectCurrentMeasurementAction);
  95. const dispatch = useDispatch();
  96. const selectedBodyPosition = useSelector(
  97. (state: RootState) => state.bodyPositionList.selectedBodyPosition
  98. );
  99. console.log(`[ViewerContainer] rerendered]`);
  100. // 当 imageUrls 改变时,更新 Redux 中的 allViewers
  101. useEffect(() => {
  102. if (imageUrls.length > 0 && JSON.stringify(imageUrls) !== JSON.stringify(allViewerUrls)) {
  103. dispatch(setAllViewers(imageUrls));
  104. }
  105. }, [imageUrls, allViewerUrls, dispatch]);
  106. // 将 imageUrl 转换为 viewportId
  107. const getViewportIdByUrl = (url: string): string | null => {
  108. return `viewport-${url}`;
  109. };
  110. const renderViewers = (start: number, end: number) => {
  111. console.log(`Rendering viewers from ${start} to ${end}`);
  112. return imageUrls.slice(start, end).map((url, index) => (
  113. <div
  114. key={start + index}
  115. onClick={(event) => handleSelectViewer(url, event)}
  116. >
  117. <StackViewer
  118. key={start + index}
  119. imageIndex={0}
  120. imageUrls={[url]}
  121. viewportId={getViewportIdByUrl(url) as string}
  122. renderingEngineId={renderingEngineId}
  123. selected={selectedViewerUrls.includes(url)}
  124. />
  125. </div>
  126. ));
  127. };
  128. useEffect(() => {
  129. renderGrid();
  130. }, [selectedBodyPosition, gridLayout]);
  131. useEffect(() => {
  132. if (action) {
  133. console.log(
  134. `[ViewerContainer] 处理功能操作: ${action}, selectedViewers:`,
  135. selectedViewerUrls
  136. );
  137. // 将选中的 imageUrl 转换为 viewportId
  138. const selectedViewportIds = selectedViewerUrls
  139. .map(getViewportIdByUrl)
  140. .filter((id): id is string => id !== null);
  141. // Handle the action
  142. switch (action) {
  143. case 'Add L Mark':
  144. selectedViewportIds.forEach((viewportId) => {
  145. addLMark(viewportId);
  146. });
  147. break;
  148. case 'Add R Mark':
  149. selectedViewportIds.forEach((viewportId) => {
  150. addRLabel(viewportId);
  151. });
  152. console.log('Adding R Mark');
  153. break;
  154. case 'Delete Selected Mark': {
  155. selectedViewportIds.forEach((viewportId) => {
  156. deleteSelectedMark(viewportId);
  157. });
  158. break;
  159. }
  160. case 'Horizontal Flip': {
  161. console.log(`开始竖直翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
  162. selectedViewportIds.forEach((viewportId) => {
  163. HorizontalFlip(viewportId);
  164. });
  165. break;
  166. }
  167. case 'Vertical Flip': {
  168. console.log(`开始水平翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
  169. selectedViewportIds.forEach((viewportId) => {
  170. VerticalFlip(viewportId);
  171. });
  172. break;
  173. }
  174. case 'Rotate Counterclockwise 90': {
  175. selectedViewportIds.forEach((viewportId) => {
  176. RotateCounterclockwise90(viewportId);
  177. });
  178. break;
  179. }
  180. case 'Rotate Clockwise 90':
  181. selectedViewportIds.forEach((viewportId) => {
  182. RotateClockwise90(viewportId);
  183. });
  184. break;
  185. case 'Rotate Any Angle':
  186. selectedViewportIds.forEach((viewportId) => {
  187. rotateAnyAngle(viewportId);
  188. });
  189. break;
  190. case 'AddMask':
  191. selectedViewportIds.forEach((viewportId) => {
  192. addMask(viewportId);
  193. });
  194. break;
  195. case 'Delete Digital Mask':
  196. selectedViewportIds.forEach((viewportId) => {
  197. remoteMask(viewportId);
  198. });
  199. break;
  200. case 'Adjust Brightness and Contrast':
  201. selectedViewportIds.forEach((viewportId) => {
  202. adjustBrightnessAndContrast(viewportId);
  203. });
  204. break;
  205. case 'Crop Selected Area':
  206. console.log('Cropping Selected Area');
  207. break;
  208. case 'Delete Mask':
  209. console.log('Deleting Mask');
  210. break;
  211. case 'Image Comparison':
  212. console.log('Comparing Images');
  213. break;
  214. case 'Invert Contrast':
  215. selectedViewportIds.forEach((viewportId) => {
  216. invertContrast(viewportId);
  217. });
  218. console.log('Inverting Contrast');
  219. break;
  220. case '1x1 Layout':
  221. dispatch(setGridLayoutAction('1x1'));
  222. console.log(`1x1 Layout`);
  223. break;
  224. case '1x2 Layout':
  225. dispatch(setGridLayoutAction('1x2'));
  226. break;
  227. case '2x1 Layout':
  228. dispatch(setGridLayoutAction('2x1'));
  229. break;
  230. case '2x2 Layout':
  231. dispatch(setGridLayoutAction('2x2'));
  232. break;
  233. case 'Magnifier': {
  234. selectedViewportIds.forEach((viewportId) => {
  235. activateMagnifier(viewportId);
  236. });
  237. break;
  238. }
  239. case 'Fit Size': {
  240. selectedViewportIds.forEach((viewportId) => {
  241. fitImageSize(viewportId);
  242. });
  243. console.log('Fitting Image Size');
  244. break;
  245. }
  246. case 'Original Size': {
  247. selectedViewportIds.forEach((viewportId) => {
  248. setOriginalSize(viewportId);
  249. });
  250. console.log('Setting Image to Original Size');
  251. break;
  252. }
  253. case 'Zoom Image':
  254. console.log('Zooming Image');
  255. break;
  256. case 'Reset Cursor':
  257. console.log('Resetting Cursor');
  258. break;
  259. case 'Pan':
  260. console.log('Panning Image');
  261. break;
  262. case 'Invert Image':
  263. selectedViewportIds.forEach((viewportId) => {
  264. InvertImage(viewportId);
  265. });
  266. break;
  267. case 'Reset Image':
  268. selectedViewportIds.forEach((viewportId) => {
  269. ResetImage(viewportId);
  270. });
  271. break;
  272. case 'Snapshot':
  273. console.log('Taking Snapshot');
  274. break;
  275. case 'Advanced Processing':
  276. console.log('Performing Advanced Processing');
  277. break;
  278. case 'Musician':
  279. console.log('Activating Musician');
  280. break;
  281. case 'Image Measurement':
  282. console.log('Measuring Image');
  283. break;
  284. case 'More':
  285. console.log('Showing More Options');
  286. break;
  287. case 'Apply Colormap':
  288. selectedViewportIds.forEach((viewportId) => {
  289. ApplyColormap(viewportId);
  290. });
  291. break;
  292. // ==================== 线段测量相关操作 ====================
  293. case '线段测量':
  294. if (selectedViewportIds.length > 0) {
  295. selectedViewportIds.forEach((viewportId) => {
  296. activateLengthMeasurement(viewportId);
  297. });
  298. } else {
  299. // 如果没有选中的 viewport,为所有可见的 viewport 激活
  300. const visibleViewportCount = getVisibleViewportCount();
  301. for (let i = 0; i < visibleViewportCount; i++) {
  302. activateLengthMeasurement(`viewport-${i}`);
  303. }
  304. }
  305. console.log('Activating Length Measurement');
  306. break;
  307. case '清除测量':
  308. if (selectedViewportIds.length > 0) {
  309. selectedViewportIds.forEach((viewportId) => {
  310. clearLengthMeasurements(viewportId);
  311. });
  312. } else {
  313. // 如果没有选中的 viewport,清除所有可见 viewport 的测量
  314. const visibleViewportCount = getVisibleViewportCount();
  315. for (let i = 0; i < visibleViewportCount; i++) {
  316. clearLengthMeasurements(`viewport-${i}`);
  317. }
  318. }
  319. console.log('Clearing Length Measurements');
  320. break;
  321. case '停用线段测量':
  322. if (selectedViewportIds.length > 0) {
  323. selectedViewportIds.forEach((viewportId) => {
  324. deactivateLengthMeasurement(viewportId);
  325. });
  326. } else {
  327. // 如果没有选中的 viewport,停用所有可见 viewport 的测量工具
  328. const visibleViewportCount = getVisibleViewportCount();
  329. for (let i = 0; i < visibleViewportCount; i++) {
  330. deactivateLengthMeasurement(`viewport-${i}`);
  331. }
  332. }
  333. console.log('Deactivating Length Measurement');
  334. break;
  335. default:
  336. break;
  337. }
  338. dispatch(clearAction()); //清理后可连续同一个action触发响应
  339. }
  340. }, [action, selectedViewerUrls, dispatch]);
  341. // ==================== 测量面板 Action 处理 ====================
  342. useEffect(() => {
  343. if (measurementAction) {
  344. console.log(`[ViewerContainer] 处理测量操作: ${measurementAction}`);
  345. // 将选中的 imageUrl 转换为 viewportId
  346. const selectedViewportIds = selectedViewerUrls
  347. .map(getViewportIdByUrl)
  348. .filter((id): id is string => id !== null);
  349. // 处理测量相关操作
  350. switch (measurementAction) {
  351. case '线段测量':
  352. if (selectedViewportIds.length > 0) {
  353. selectedViewportIds.forEach((viewportId) => {
  354. const success = activateLengthMeasurement(viewportId);
  355. if (success) {
  356. dispatch(
  357. setToolActive({
  358. toolName: 'LengthTool',
  359. viewportId: viewportId,
  360. })
  361. );
  362. }
  363. });
  364. } else {
  365. // 如果没有选中的 viewport,为所有可见的 viewport 激活
  366. const visibleViewportCount = getVisibleViewportCount();
  367. for (let i = 0; i < visibleViewportCount; i++) {
  368. const success = activateLengthMeasurement(`viewport-${i}`);
  369. if (success) {
  370. dispatch(
  371. setToolActive({
  372. toolName: 'LengthTool',
  373. viewportId: `viewport-${i}`,
  374. })
  375. );
  376. }
  377. }
  378. }
  379. console.log('Activating Length Measurement from MeasurementPanel');
  380. break;
  381. case '清除测量':
  382. if (selectedViewportIds.length > 0) {
  383. MeasurementToolManager.clearLengthMeasurementsForViewports(selectedViewportIds);
  384. MeasurementToolManager.clearAngleMeasurementsForViewports(selectedViewportIds);
  385. }
  386. console.log('Clearing Length Measurements from MeasurementPanel');
  387. break;
  388. case '停用线段测量':
  389. if (selectedViewportIds.length > 0) {
  390. selectedViewportIds.forEach((viewportId) => {
  391. const success = deactivateLengthMeasurement(viewportId);
  392. if (success) {
  393. dispatch(
  394. setToolInactive({
  395. toolName: 'LengthTool',
  396. viewportId: viewportId,
  397. })
  398. );
  399. }
  400. });
  401. }
  402. console.log('Deactivating Length Measurement from MeasurementPanel');
  403. break;
  404. case '角度测量':
  405. console.log(`开始角度测量`);
  406. if (selectedViewportIds.length > 0) {
  407. selectedViewportIds.forEach((viewportId) => {
  408. const success = activateAngleMeasurement(viewportId);
  409. if (success) {
  410. console.log(`激活角度测量工具成功`);
  411. dispatch(
  412. setToolActive({
  413. toolName: 'AngleTool',
  414. viewportId: viewportId,
  415. })
  416. );
  417. }
  418. });
  419. }
  420. console.log('Activating Angle Measurement from MeasurementPanel');
  421. break;
  422. case '测量校正':
  423. console.log('Measurement Calibration - 功能待实现');
  424. break;
  425. default:
  426. console.log(`未处理的测量操作: ${measurementAction}`);
  427. break;
  428. }
  429. // 清理测量 action
  430. dispatch(clearMeasurementAction());
  431. }
  432. }, [measurementAction, selectedViewerUrls, gridLayout, dispatch]);
  433. /**
  434. * 获取当前可见的 viewport 数量
  435. */
  436. const getVisibleViewportCount = (): number => {
  437. switch (gridLayout) {
  438. case '1x1':
  439. return 1;
  440. case '1x2':
  441. case '2x1':
  442. return Math.min(2, imageUrls.length);
  443. case '2x2':
  444. return Math.min(4, imageUrls.length);
  445. default:
  446. return 1;
  447. }
  448. };
  449. const handleSelectViewer = (imageUrl: string, event: React.MouseEvent) => {
  450. console.log(`handleSelectViewer : ${imageUrl}`);
  451. console.log(`selectedViewers 旧值:`, selectedViewerUrls);
  452. dispatch(toggleViewerSelection(imageUrl));
  453. };
  454. const renderGrid = () => {
  455. console.log('Rendering layout', gridLayout);
  456. switch (gridLayout) {
  457. case '1x1': {
  458. //变成单分格时,自动选中第一个
  459. dispatch(toggleViewerSelection(allViewerUrls[0]));
  460. return (
  461. <div
  462. className="h-full w-full"
  463. style={{ display: 'grid', gridTemplateColumns: '1fr' }}
  464. >
  465. {renderViewers(0, 1)}
  466. </div>
  467. );
  468. break;
  469. }
  470. case '1x2':
  471. return (
  472. <div
  473. className="h-full w-full"
  474. style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
  475. >
  476. {renderViewers(0, 2)}
  477. </div>
  478. );
  479. case '2x1':
  480. return (
  481. <div className="h-full w-full grid grid-cols-1 grid-rows-2">
  482. {renderViewers(0, 2)}
  483. </div>
  484. );
  485. case '2x2':
  486. return (
  487. <div
  488. className="h-full w-full"
  489. style={{
  490. display: 'grid',
  491. gridTemplateColumns: '1fr 1fr',
  492. gridTemplateRows: '1fr 1fr',
  493. }}
  494. >
  495. {renderViewers(0, 4)}
  496. </div>
  497. );
  498. default:
  499. return null;
  500. }
  501. };
  502. return <>{renderGrid()}</>;
  503. };
  504. export default ViewerContainer;