ViewerContainer.tsx 18 KB

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