Compare commits
108
Commits
v3.8.0-beta.71
...
v3.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a57e54f74 | ||
|
|
aebcf962bb | ||
|
|
b7ebb0aa8f | ||
|
|
29c02121df | ||
|
|
a3d7f88b03 | ||
|
|
a3d8129089 | ||
|
|
b427af622e | ||
|
|
634abf353f | ||
|
|
471e6a1559 | ||
|
|
2f811470a8 | ||
|
|
6262a7f9d4 | ||
|
|
53c2740d75 | ||
|
|
85a82fac0e | ||
|
|
4c8a808a12 | ||
|
|
711c177cf2 | ||
|
|
f82ab4fda4 | ||
|
|
2d4e049d8a | ||
|
|
9f5165de47 | ||
|
|
1b290bdaa1 | ||
|
|
8feef28bff | ||
|
|
60d68e3410 | ||
|
|
3539e04019 | ||
|
|
c11c3b7e61 | ||
|
|
4029a8445b | ||
|
|
19bdcbb453 | ||
|
|
0386c29480 | ||
|
|
1dcdc9dabe | ||
|
|
feaebd7f61 | ||
|
|
6a8f4b9f38 | ||
|
|
956fff4e5b | ||
|
|
f1a736d193 | ||
|
|
4784638717 | ||
|
|
49e82f4756 | ||
|
|
c09311d3b7 | ||
|
|
b4ed80d99b | ||
|
|
c87081d924 | ||
|
|
f09dfaf911 | ||
|
|
11a4ba3c99 | ||
|
|
56ccb4f7e0 | ||
|
|
6ebd2cc7cb | ||
|
|
25c01fc66c | ||
|
|
2ea882e299 | ||
|
|
52a0c59294 | ||
|
|
3ec7e51216 | ||
|
|
1b366c92ee | ||
|
|
13d67d90fc | ||
|
|
f0ed3fd7b9 | ||
|
|
65d419dd52 | ||
|
|
4349fc1085 | ||
|
|
8192e348ec | ||
|
|
8c5ab942b9 | ||
|
|
ab19d9c0a2 | ||
|
|
d250d04580 | ||
|
|
28bc8914a8 | ||
|
|
3952d465dc | ||
|
|
1a013909a1 | ||
|
|
8f42baf36e | ||
|
|
2633ef7323 | ||
|
|
da7c1a5d8c | ||
|
|
058da93863 | ||
|
|
bc819409b3 | ||
|
|
170bb96983 | ||
|
|
c4e6556259 | ||
|
|
ab61cc9b1f | ||
|
|
3754c224b4 | ||
|
|
d3cdfd8506 | ||
|
|
aa572daa2c | ||
|
|
f99a0bfb31 | ||
|
|
a6c6fff43a | ||
|
|
e687384933 | ||
|
|
e80fc6f477 | ||
|
|
41a8d105f1 | ||
|
|
60b2f69334 | ||
|
|
df49a653be | ||
|
|
77693ddef4 | ||
|
|
6a757f69d8 | ||
|
|
2b83393f91 | ||
|
|
ac0e1c0a2c | ||
|
|
b0b65c582d | ||
|
|
4f99244d86 | ||
|
|
be9bb06565 | ||
|
|
c7e53bdbf8 | ||
|
|
7c3d67c2a8 | ||
|
|
e9c30a108e | ||
|
|
603d5cc53a | ||
|
|
5cc693d26c | ||
|
|
7fac49b449 | ||
|
|
7014e6ca4a | ||
|
|
87b68bba4a | ||
|
|
d57e8bc157 | ||
|
|
dc0b183dd4 | ||
|
|
06920ab927 | ||
|
|
dc37802ec1 | ||
|
|
894a227978 | ||
|
|
1c9fee1bec | ||
|
|
5645ac1b27 | ||
|
|
df32a05a4d | ||
|
|
e205bf5c4e | ||
|
|
5ddf8a1602 | ||
|
|
83fccad46b | ||
|
|
72ec0bffb5 | ||
|
|
82c6c3c353 | ||
|
|
1d38fe30a4 | ||
|
|
666964b6d7 | ||
|
|
fa4ee5c559 | ||
|
|
cd58560e9c | ||
|
|
16afa740b6 | ||
|
|
6715ad9bd6 |
No files matched your search
@@ -495,13 +495,18 @@ workflows:
|
||||
- BUILD:
|
||||
filters:
|
||||
branches:
|
||||
only: release
|
||||
- NPM_PUBLISH:
|
||||
only: /^release\/.*/
|
||||
- HOLD_FOR_APPROVAL:
|
||||
type: approval
|
||||
requires:
|
||||
- BUILD
|
||||
- NPM_PUBLISH:
|
||||
requires:
|
||||
- HOLD_FOR_APPROVAL
|
||||
- DOCKER_RELEASE_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
|
||||
###
|
||||
# Unit and E2E tests have already run for PR_CHECKS
|
||||
# Re-running should not gain us any confidence here
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# ABOUT:
|
||||
# https://docs.codecov.io/docs/codecov-yaml
|
||||
#
|
||||
#
|
||||
# COMMIT STATUS:
|
||||
# https://docs.codecov.io/docs/commit-status
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5%
|
||||
core:
|
||||
flags: core
|
||||
threshold: 0.5%
|
||||
viewer:
|
||||
flags: viewer
|
||||
threshold: 0.5%
|
||||
patch: off
|
||||
flags:
|
||||
core:
|
||||
paths:
|
||||
- platform/core
|
||||
viewer:
|
||||
paths:
|
||||
- platform/app
|
||||
@@ -24,6 +24,7 @@ yarn-error.log
|
||||
.DS_Store
|
||||
.env
|
||||
*.code-workspace
|
||||
.directory
|
||||
|
||||
# Common Example Data Directories
|
||||
sampledata/
|
||||
|
||||
+286
@@ -3,6 +3,292 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **contour:** set renderFill to false for contour ([#4186](https://github.com/OHIF/Viewers/issues/4186)) ([471e6a1](https://github.com/OHIF/Viewers/commit/471e6a1559d983267448058b8e17461f79130c79))
|
||||
* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([2d4e049](https://github.com/OHIF/Viewers/commit/2d4e049d8a54ca25c9dc02f29f2e53a6968d82e1))
|
||||
* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([9f5165d](https://github.com/OHIF/Viewers/commit/9f5165de47ed547919683498cfae87909de94786))
|
||||
* **docker:** docker build was broken because of imports ([#4192](https://github.com/OHIF/Viewers/issues/4192)) ([53c2740](https://github.com/OHIF/Viewers/commit/53c2740d7589439f5fa6de20baf00b766680ddee))
|
||||
* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([634abf3](https://github.com/OHIF/Viewers/commit/634abf353f02cffec462e341d594741fdefb6f22))
|
||||
* segmentation creation and segmentation mode viewport rendering ([#4193](https://github.com/OHIF/Viewers/issues/4193)) ([a3d7f88](https://github.com/OHIF/Viewers/commit/a3d7f88b038fdb8c9dbe5358402a159cdadd41c8))
|
||||
* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([1b290bd](https://github.com/OHIF/Viewers/commit/1b290bdaa1a0ba750a4e5f539a51a636de529240))
|
||||
* **seg:** should be able to navigate outside toolbox and come back later ([#4196](https://github.com/OHIF/Viewers/issues/4196)) ([6262a7f](https://github.com/OHIF/Viewers/commit/6262a7f9d4cb71d2dd9615ccc8fe25c947da65dd))
|
||||
* **select:** utilize react portals for select component ([#4144](https://github.com/OHIF/Viewers/issues/4144)) ([8feef28](https://github.com/OHIF/Viewers/commit/8feef28bff1ef4547ff744b64ffbbbfdb93d4c72))
|
||||
* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([a3d8129](https://github.com/OHIF/Viewers/commit/a3d81290897397634a246910a30190301066580c))
|
||||
* **tmtv:** side panel crashing when activeToolOptions is not an array ([#4189](https://github.com/OHIF/Viewers/issues/4189)) ([85a82fa](https://github.com/OHIF/Viewers/commit/85a82fac0e1700eb2b9964078a90aadb1877853b))
|
||||
* **tmtv:** threshold was crashing the side panel ([#4119](https://github.com/OHIF/Viewers/issues/4119)) ([c11c3b7](https://github.com/OHIF/Viewers/commit/c11c3b7e611af231773c5d52320b8b777c7c717d))
|
||||
* **us:** logic for color conversion ([b7ebb0a](https://github.com/OHIF/Viewers/commit/b7ebb0aa8f814427045b7d5e7568dc0298ab0973))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **types:** typed app config ([#4171](https://github.com/OHIF/Viewers/issues/4171)) ([4c8a808](https://github.com/OHIF/Viewers/commit/4c8a808a12718865501e362ce37c2d2f75a1a687))
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
|
||||
|
||||
# [3.7.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.7.0) (2023-10-11)
|
||||
|
||||
|
||||
|
||||
# [3.6.0](https://github.com/OHIF/Viewers/compare/v3.6.0-beta.3...v3.6.0) (2023-06-07)
|
||||
|
||||
|
||||
|
||||
# [3.5.0](https://github.com/OHIF/Viewers/compare/v3.5.0-beta.1...v3.5.0) (2023-06-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **app:** change ohif viewer to ohif app ([#3451](https://github.com/OHIF/Viewers/issues/3451)) ([16afa74](https://github.com/OHIF/Viewers/commit/16afa740b60b31037100444ef9311b80ffea2f67))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport-sync:** Enable re-sync image slices in a different position when needed ([#3984](https://github.com/OHIF/Viewers/issues/3984)) ([6ebd2cc](https://github.com/OHIF/Viewers/commit/6ebd2cc7cb70cd88fd01dc1e516077f27b201943))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **vewport:** Add missing blendmodes from cornerstonejs ([#4055](https://github.com/OHIF/Viewers/issues/4055)) ([3ec7e51](https://github.com/OHIF/Viewers/commit/3ec7e512169a07506388902acb5b2c118093fa50))
|
||||
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hp:** Fails to display any layouts in the layout selector if first layout has multiple stages ([#4058](https://github.com/OHIF/Viewers/issues/4058)) ([f0ed3fd](https://github.com/OHIF/Viewers/commit/f0ed3fd7b99b0e4e00b261ceb9888ba94726719c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-video:** Update get direct func for dicom json to use url if present and fix config argument ([#4017](https://github.com/OHIF/Viewers/issues/4017)) ([4f99244](https://github.com/OHIF/Viewers/commit/4f99244d864427d69be6f863cb7a6a78411adb12))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MetaDataProvider:** Fix tag in GeneralImageModule ([#4000](https://github.com/OHIF/Viewers/issues/4000)) ([e9c30a1](https://github.com/OHIF/Viewers/commit/e9c30a108e2dd14a8b137b81e5b832cc167bc3d1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Microscopy bulkdata and image retrieve ([#3894](https://github.com/OHIF/Viewers/issues/3894)) ([7fac49b](https://github.com/OHIF/Viewers/commit/7fac49b4492b4bd5e9ece8e2e2b0fa2faa840d7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
|
||||
|
||||
|
||||
|
||||
@@ -41,12 +41,15 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
|
||||
|
||||
| | | |
|
||||
| :-: | :--- | :--- |
|
||||
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
|
||||
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-measurements.webp?raw=true" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-segmentation.webp?raw=true" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ptct.webp?raw=true" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
|
||||
|
||||
|
||||
## About
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1
|
||||
b7ebb0aa8f814427045b7d5e7568dc0298ab0973
|
||||
@@ -3,6 +3,212 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([634abf3](https://github.com/OHIF/Viewers/commit/634abf353f02cffec462e341d594741fdefb6f22))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||
"version": "3.8.0-beta.71",
|
||||
"version": "3.8.1",
|
||||
"description": "DICOM RT read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.0-beta.71",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.71",
|
||||
"@ohif/extension-default": "3.8.0-beta.71",
|
||||
"@ohif/i18n": "3.8.0-beta.71",
|
||||
"@ohif/core": "3.8.1\n",
|
||||
"@ohif/extension-cornerstone": "3.8.1\n",
|
||||
"@ohif/extension-default": "3.8.1\n",
|
||||
"@ohif/i18n": "3.8.1\n",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
+13
-6
@@ -5,9 +5,13 @@ import loadRTStruct from './loadRTStruct';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
|
||||
|
||||
let loadPromises = {};
|
||||
const loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
@@ -69,8 +73,8 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
@@ -90,7 +94,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
|
||||
const { SOPInstanceUID } = rtDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
if (
|
||||
@@ -168,7 +172,10 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(rtDisplaySet, segmentationService) {
|
||||
function _segmentationExistsInCache(
|
||||
rtDisplaySet,
|
||||
segmentationService: AppTypes.SegmentationService
|
||||
) {
|
||||
// Todo: fix this
|
||||
return false;
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
@@ -13,7 +13,7 @@ function promptHydrateRT({
|
||||
toolGroupId = 'default',
|
||||
preHydrateCallbacks,
|
||||
hydrateRTDisplaySet,
|
||||
}) {
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
|
||||
return new Promise(async function (resolve, reject) {
|
||||
@@ -36,7 +36,7 @@ function promptHydrateRT({
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
@@ -57,6 +57,7 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
id: 'promptHydrateRT',
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
@@ -66,6 +67,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
function OHIFCornerstoneRTViewport(props) {
|
||||
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
@@ -111,12 +111,15 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
viewportType: 'stack',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,244 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([2d4e049](https://github.com/OHIF/Viewers/commit/2d4e049d8a54ca25c9dc02f29f2e53a6968d82e1))
|
||||
* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([9f5165d](https://github.com/OHIF/Viewers/commit/9f5165de47ed547919683498cfae87909de94786))
|
||||
* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([634abf3](https://github.com/OHIF/Viewers/commit/634abf353f02cffec462e341d594741fdefb6f22))
|
||||
* segmentation creation and segmentation mode viewport rendering ([#4193](https://github.com/OHIF/Viewers/issues/4193)) ([a3d7f88](https://github.com/OHIF/Viewers/commit/a3d7f88b038fdb8c9dbe5358402a159cdadd41c8))
|
||||
* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([1b290bd](https://github.com/OHIF/Viewers/commit/1b290bdaa1a0ba750a4e5f539a51a636de529240))
|
||||
* **us:** logic for color conversion ([b7ebb0a](https://github.com/OHIF/Viewers/commit/b7ebb0aa8f814427045b7d5e7568dc0298ab0973))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.8.0-beta.71",
|
||||
"version": "3.8.1",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.0-beta.71",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.71",
|
||||
"@ohif/extension-default": "3.8.0-beta.71",
|
||||
"@ohif/i18n": "3.8.0-beta.71",
|
||||
"@ohif/core": "3.8.1\n",
|
||||
"@ohif/extension-cornerstone": "3.8.1\n",
|
||||
"@ohif/extension-default": "3.8.1\n",
|
||||
"@ohif/i18n": "3.8.1\n",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.67.0",
|
||||
"@cornerstonejs/core": "^1.67.0",
|
||||
"@kitware/vtk.js": "29.7.0",
|
||||
"@cornerstonejs/adapters": "^1.77.6",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
import { Types } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import {
|
||||
segmentation as cornerstoneToolsSegmentation,
|
||||
@@ -48,7 +48,7 @@ const commandsModule = ({
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
} = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
@@ -94,6 +94,7 @@ const commandsModule = ({
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const currentSegmentations = segmentationService.getSegmentations();
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
@@ -213,7 +214,7 @@ const commandsModule = ({
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const segDisplaySet = displaySet;
|
||||
const suppressEvents = false;
|
||||
@@ -434,30 +435,6 @@ const commandsModule = ({
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleThresholdRangeAndDynamic() {
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
|
||||
if (!toolGroupIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id);
|
||||
|
||||
brushInstances.forEach(({ configuration }) => {
|
||||
const { activeStrategy, strategySpecificConfiguration } = configuration;
|
||||
|
||||
if (activeStrategy.startsWith('THRESHOLD')) {
|
||||
const thresholdConfig = strategySpecificConfiguration.THRESHOLD;
|
||||
|
||||
if (thresholdConfig) {
|
||||
thresholdConfig.isDynamic = !thresholdConfig.isDynamic;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@@ -491,9 +468,6 @@ const commandsModule = ({
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
toggleThresholdRangeAndDynamic: {
|
||||
commandFn: actions.toggleThresholdRangeAndDynamic,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
import { useAppConfig } from '@state';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import { SegmentationPanelMode } from './types/segmentation';
|
||||
|
||||
const getPanelModule = ({
|
||||
commandsManager,
|
||||
@@ -11,17 +10,12 @@ const getPanelModule = ({
|
||||
extensionManager,
|
||||
configuration,
|
||||
title,
|
||||
}) => {
|
||||
}: withAppTypes) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
||||
const segmentationPanelMode =
|
||||
customizationService.get('segmentation.segmentationPanelMode')?.value ||
|
||||
SegmentationPanelMode.Dropdown;
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
@@ -29,8 +23,8 @@ const getPanelModule = ({
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
|
||||
segmentationPanelMode: segmentationPanelMode,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -38,9 +32,6 @@ const getPanelModule = ({
|
||||
|
||||
const wrappedPanelSegmentationWithTools = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
const segmentationPanelMode =
|
||||
customizationService.get('segmentation.segmentationPanelMode')?.value ||
|
||||
SegmentationPanelMode.Dropdown;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -60,7 +51,8 @@ const getPanelModule = ({
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
segmentationPanelMode: segmentationPanelMode,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
+19
-5
@@ -8,9 +8,13 @@ import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
|
||||
|
||||
let loadPromises = {};
|
||||
const loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
@@ -95,7 +99,12 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(
|
||||
segDisplaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers
|
||||
) {
|
||||
const { SOPInstanceUID } = segDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
@@ -137,7 +146,12 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
|
||||
async function _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
}: withAppTypes) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
@@ -212,7 +226,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
|
||||
Object.assign(segDisplaySet, results);
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const { segmentationService, toolGroupService } = servicesManager.services;
|
||||
export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
@@ -20,12 +20,16 @@ export function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const toolName = getToolNameForButton(button);
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
if (!toolGroup.hasTool(toolName) && !toolNames) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
@@ -51,17 +55,3 @@ export function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getToolNameForButton(button) {
|
||||
const { props } = button;
|
||||
|
||||
const commands = props?.commands || button.commands;
|
||||
|
||||
if (commands && commands.length) {
|
||||
const command = commands[0];
|
||||
const { commandOptions } = command;
|
||||
const { toolName } = commandOptions || { toolName: props?.id ?? button.id };
|
||||
return toolName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import { getToolbarModule } from './getToolbarModule';
|
||||
import preRegistration from './init';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
||||
@@ -29,7 +28,6 @@ const extension = {
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
preRegistration,
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { addTool, BrushTool } from '@cornerstonejs/tools';
|
||||
|
||||
export default function init({ servicesManager }): void {
|
||||
addTool(BrushTool);
|
||||
}
|
||||
@@ -17,12 +17,19 @@ export default function PanelSegmentation({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
}) {
|
||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
||||
}: withAppTypes) {
|
||||
const {
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
|
||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
@@ -52,6 +59,64 @@ export default function PanelSegmentation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// temporary measure to not allow add segmentation when the selected viewport
|
||||
// is stack viewport
|
||||
useEffect(() => {
|
||||
const handleActiveViewportChange = viewportId => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
||||
viewportId || viewportGridService.getActiveViewportId()
|
||||
);
|
||||
|
||||
if (!displaySetUIDs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isReconstructable =
|
||||
displaySetUIDs?.some(displaySetUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
|
||||
return displaySet?.isReconstructable;
|
||||
}) || false;
|
||||
|
||||
if (isReconstructable) {
|
||||
setAddSegmentationClassName('');
|
||||
} else {
|
||||
setAddSegmentationClassName('ohif-disabled');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial state
|
||||
handleActiveViewportChange();
|
||||
|
||||
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
|
||||
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
|
||||
|
||||
const subsGrid = [];
|
||||
[ready, changedGrid].forEach(evt => {
|
||||
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
|
||||
handleActiveViewportChange(viewportId);
|
||||
});
|
||||
|
||||
subsGrid.push(unsubscribe);
|
||||
});
|
||||
|
||||
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
|
||||
const subsData = [];
|
||||
[changedData].forEach(evt => {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
|
||||
handleActiveViewportChange();
|
||||
});
|
||||
|
||||
subsData.push(unsubscribe);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
subsGrid.forEach(unsub => unsub());
|
||||
subsData.forEach(unsub => unsub());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
@@ -59,7 +124,9 @@ export default function PanelSegmentation({
|
||||
};
|
||||
|
||||
const onSegmentationAdd = async () => {
|
||||
commandsManager.runCommand('createEmptySegmentationForViewport');
|
||||
commandsManager.runCommand('createEmptySegmentationForViewport', {
|
||||
viewportId: viewportGridService.getActiveViewportId(),
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentationClick = (segmentationId: string) => {
|
||||
@@ -152,6 +219,7 @@ export default function PanelSegmentation({
|
||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
// segment hide
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
@@ -242,7 +310,13 @@ export default function PanelSegmentation({
|
||||
});
|
||||
};
|
||||
|
||||
const SegmentationGroupTableComponent = components[configuration?.segmentationPanelMode];
|
||||
const SegmentationGroupTableComponent =
|
||||
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
|
||||
const allowAddSegment = configuration?.addSegment;
|
||||
const onSegmentationAddWrapper =
|
||||
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
|
||||
? configuration?.onSegmentationAdd
|
||||
: onSegmentationAdd;
|
||||
|
||||
return (
|
||||
<SegmentationGroupTableComponent
|
||||
@@ -250,7 +324,9 @@ export default function PanelSegmentation({
|
||||
segmentations={segmentations}
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAdd}
|
||||
onSegmentationAdd={onSegmentationAddWrapper}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
showAddSegment={allowAddSegment}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Enums, cache } from '@cornerstonejs/core';
|
||||
* @param params.viewportId - ID of the viewport to be updated.
|
||||
* @param params.loadFn - Function to load the segmentation data.
|
||||
* @param params.servicesManager - The services manager.
|
||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
||||
* @param params.displaySet - the display set.
|
||||
*
|
||||
* @returns Returns true upon successful update of viewports for segmentation rendering.
|
||||
*/
|
||||
@@ -18,12 +18,12 @@ async function updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
loadFn,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID,
|
||||
displaySet,
|
||||
}: {
|
||||
viewportId: string;
|
||||
loadFn: () => Promise<string>;
|
||||
servicesManager: any;
|
||||
referencedDisplaySetInstanceUID?: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
displaySet?: any;
|
||||
}) {
|
||||
const { cornerstoneViewportService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
@@ -31,13 +31,13 @@ async function updateViewportsForSegmentationRendering({
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
referencedDisplaySetInstanceUID =
|
||||
referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
||||
const referencedDisplaySetInstanceUID =
|
||||
displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
||||
|
||||
const updatedViewports = getUpdatedViewportsForSegmentation({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
referencedDisplaySetInstanceUID,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
});
|
||||
|
||||
// create Segmentation callback which needs to be waited until
|
||||
@@ -56,7 +56,7 @@ async function updateViewportsForSegmentationRendering({
|
||||
updatedViewports.forEach(async viewport => {
|
||||
viewport.viewportOptions = {
|
||||
...viewport.viewportOptions,
|
||||
viewportType: 'volume',
|
||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
||||
needsRerendering: true,
|
||||
};
|
||||
const viewportId = viewport.viewportId;
|
||||
@@ -66,7 +66,7 @@ async function updateViewportsForSegmentationRendering({
|
||||
|
||||
// only run the createSegmentationForVolume for the targetViewportId
|
||||
// since the rest will get handled by cornerstoneViewportService
|
||||
if (volumeExists && viewportId === targetViewportId) {
|
||||
if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) {
|
||||
await createSegmentationForVolume();
|
||||
return;
|
||||
}
|
||||
@@ -126,19 +126,19 @@ const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the ID of the viewport to be updated.
|
||||
* @param params.servicesManager - The services manager
|
||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
||||
* @param params.displaySet - the display set.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
function getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID,
|
||||
}) {
|
||||
displaySet,
|
||||
}: withAppTypes) {
|
||||
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
|
||||
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
@@ -146,14 +146,15 @@ function getUpdatedViewportsForSegmentation({
|
||||
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
|
||||
|
||||
const referenceDisplaySetInstanceUID =
|
||||
referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
||||
displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
|
||||
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
targetViewportId,
|
||||
referenceDisplaySetInstanceUID
|
||||
referenceDisplaySetInstanceUID,
|
||||
isHangingProtocolLayout
|
||||
);
|
||||
|
||||
viewports.forEach((viewport, viewportId) => {
|
||||
@@ -174,7 +175,7 @@ function getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
||||
needsRerendering: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ function promptHydrateSEG({
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
hydrateSEGDisplaySet,
|
||||
}) {
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
|
||||
return new Promise(async function (resolve, reject) {
|
||||
@@ -63,6 +63,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
|
||||
function OHIFCornerstoneSEGViewport(props) {
|
||||
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
@@ -25,7 +25,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
displaySetService,
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
@@ -112,7 +111,10 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,239 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([2d4e049](https://github.com/OHIF/Viewers/commit/2d4e049d8a54ca25c9dc02f29f2e53a6968d82e1))
|
||||
* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([9f5165d](https://github.com/OHIF/Viewers/commit/9f5165de47ed547919683498cfae87909de94786))
|
||||
* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([1b290bd](https://github.com/OHIF/Viewers/commit/1b290bdaa1a0ba750a4e5f539a51a636de529240))
|
||||
* **us:** logic for color conversion ([b7ebb0a](https://github.com/OHIF/Viewers/commit/b7ebb0aa8f814427045b7d5e7568dc0298ab0973))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.8.0-beta.71",
|
||||
"version": "3.8.1",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,10 +34,10 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.0-beta.71",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.71",
|
||||
"@ohif/extension-measurement-tracking": "3.8.0-beta.71",
|
||||
"@ohif/ui": "3.8.0-beta.71",
|
||||
"@ohif/core": "3.8.1\n",
|
||||
"@ohif/extension-cornerstone": "3.8.1\n",
|
||||
"@ohif/extension-measurement-tracking": "3.8.1\n",
|
||||
"@ohif/ui": "3.8.1\n",
|
||||
"dcmjs": "^0.29.12",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.67.0",
|
||||
"@cornerstonejs/core": "^1.67.0",
|
||||
"@cornerstonejs/tools": "^1.67.0",
|
||||
"@cornerstonejs/adapters": "^1.77.6",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@cornerstonejs/tools": "^1.77.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -40,7 +40,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
||||
return dataset;
|
||||
};
|
||||
|
||||
const commandsModule = props => {
|
||||
const commandsModule = (props: withAppTypes) => {
|
||||
const { servicesManager } = props;
|
||||
const { customizationService } = servicesManager.services;
|
||||
const actions = {
|
||||
@@ -57,7 +57,7 @@ const commandsModule = props => {
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
var objectUrl = URL.createObjectURL(reportBlob);
|
||||
const objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import addMeasurement from './utils/addMeasurement';
|
||||
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
@@ -85,7 +85,11 @@ function addInstances(instances: InstanceMetadata[], displaySetService: DisplayS
|
||||
* @param servicesManager is the services that can be used for creating
|
||||
* @returns The list of display sets created for the given instances object
|
||||
*/
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
// If the series has no instances, stop here
|
||||
if (!instances || !instances.length) {
|
||||
throw new Error('No instances were provided');
|
||||
@@ -150,13 +154,37 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(displaySet, servicesManager, extensionManager) {
|
||||
async function _load(displaySet, servicesManager: AppTypes.ServicesManager, extensionManager) {
|
||||
const { displaySetService, measurementService } = servicesManager.services;
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
|
||||
const { ContentSequence } = displaySet.instance;
|
||||
|
||||
async function retrieveBulkData(obj, parentObj = null, key = null) {
|
||||
for (const prop in obj) {
|
||||
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
|
||||
await retrieveBulkData(obj[prop], obj, prop);
|
||||
} else if (Array.isArray(obj[prop])) {
|
||||
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
|
||||
} else if (prop === 'BulkDataURI') {
|
||||
const value = await dataSource.retrieve.bulkDataURI({
|
||||
BulkDataURI: obj[prop],
|
||||
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
|
||||
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
|
||||
});
|
||||
if (parentObj && key) {
|
||||
parentObj[key] = new Float32Array(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displaySet.isLoaded !== true) {
|
||||
await retrieveBulkData(ContentSequence);
|
||||
}
|
||||
|
||||
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||
|
||||
@@ -199,7 +227,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
newDisplaySet,
|
||||
dataSource,
|
||||
servicesManager
|
||||
servicesManager: AppTypes.ServicesManager
|
||||
) {
|
||||
const { customizationService } = servicesManager.services;
|
||||
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||
@@ -211,7 +239,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newDisplaySet instanceof ImageSet) {
|
||||
if ((!newDisplaySet) instanceof ImageSet) {
|
||||
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
||||
return;
|
||||
}
|
||||
@@ -275,7 +303,22 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
}
|
||||
|
||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
|
||||
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
|
||||
const frame =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
|
||||
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
|
||||
|
||||
/** Update measurement properties */
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frame;
|
||||
delete measurement.coords;
|
||||
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
}
|
||||
@@ -291,7 +334,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
||||
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
||||
const ReferencedFrameNumber =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
|
||||
import { srProtocol } from './getHangingProtocolModule';
|
||||
import onModeEnter from './onModeEnter';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import preRegistration from './init';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
addTool,
|
||||
AngleTool,
|
||||
annotation,
|
||||
ArrowAnnotateTool,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
CircleROITool,
|
||||
LengthTool,
|
||||
PlanarFreehandROITool,
|
||||
RectangleROITool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||
import addToolInstance from './utils/addToolInstance';
|
||||
@@ -19,24 +19,25 @@ import toolNames from './tools/toolNames';
|
||||
* @param {object} configuration
|
||||
*/
|
||||
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
||||
addTool(DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool);
|
||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
|
||||
|
||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
|
||||
// TODO - fix the rehydration of Freehand, as it throws an exception
|
||||
// on a missing polyline. The fix is probably in CS3D
|
||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const dashedLine = {
|
||||
lineDash: '4,4',
|
||||
};
|
||||
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
|
||||
[toolNames.DICOMSRDisplay]: dashedLine,
|
||||
SRLength: dashedLine,
|
||||
SRBidirectional: dashedLine,
|
||||
SREllipticalROI: dashedLine,
|
||||
@@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
|
||||
SRCobbAngle: dashedLine,
|
||||
SRAngle: dashedLine,
|
||||
SRPlanarFreehandROI: dashedLine,
|
||||
SRRectangleROI: dashedLine,
|
||||
global: {},
|
||||
});
|
||||
}
|
||||
@@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
}
|
||||
|
||||
_getTextBoxLinesFromLabels(labels) {
|
||||
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 3);
|
||||
const labelLength = Math.min(labels.length, 5);
|
||||
const lines = [];
|
||||
|
||||
for (let i = 0; i < labelLength; i++) {
|
||||
const labelEntry = labels[i];
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
@@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
|
||||
// Filter toolData to only render the data for the active SR.
|
||||
const filteredAnnotations = annotations.filter(annotation =>
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
|
||||
);
|
||||
|
||||
if (!viewport._actors?.size) {
|
||||
@@ -77,20 +77,24 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
toolName: this.getToolName(),
|
||||
viewportId: enabledElement.viewport.id,
|
||||
};
|
||||
const { style: annotationStyle } = annotation.config;
|
||||
|
||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||
const annotation = filteredAnnotations[i];
|
||||
const annotationUID = annotation.annotationUID;
|
||||
const { renderableData } = annotation.data.cachedStats;
|
||||
const { cachedStats } = annotation.data;
|
||||
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
|
||||
const { referencedImageId } = annotation.metadata;
|
||||
|
||||
styleSpecifier.annotationUID = annotationUID;
|
||||
|
||||
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
|
||||
this.getToolName()
|
||||
];
|
||||
|
||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||
const color =
|
||||
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
? 'rgb(0, 255, 0)'
|
||||
: this.getStyle('color', styleSpecifier, annotation);
|
||||
|
||||
@@ -98,6 +102,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
color,
|
||||
lineDash,
|
||||
lineWidth,
|
||||
...groupStyle,
|
||||
};
|
||||
|
||||
Object.keys(renderableData).forEach(GraphicType => {
|
||||
@@ -160,6 +165,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
const drawingOptions = {
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
};
|
||||
let allCanvasCoordinates = [];
|
||||
renderableData.map((data, index) => {
|
||||
@@ -307,6 +313,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
{
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
+18
-36
@@ -1,15 +1,13 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import { Types, annotation } from '@cornerstonejs/tools';
|
||||
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
import toolNames from '../tools/toolNames';
|
||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
|
||||
// TODO -> Render rotated ellipse .
|
||||
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
|
||||
const toolName = toolNames.DICOMSRDisplay;
|
||||
|
||||
const measurementData = {
|
||||
@@ -27,26 +25,25 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
|
||||
}
|
||||
|
||||
measurementData.renderableData[GraphicType].push(
|
||||
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
|
||||
_getRenderableData(GraphicType, GraphicData, imageId)
|
||||
);
|
||||
});
|
||||
|
||||
// Use the metadata provider to grab its imagePlaneModule metadata
|
||||
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
|
||||
// Create Cornerstone3D Annotation from measurement
|
||||
const frameNumber =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
/**
|
||||
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
|
||||
* This is used before the annotation is hydrated. If hydrated the measurement will be added
|
||||
* to the measurement service and will be available for the other viewports.
|
||||
*/
|
||||
const SRAnnotation: Types.Annotation = {
|
||||
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||
highlighted: false,
|
||||
isLocked: false,
|
||||
invalidated: false,
|
||||
metadata: {
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
toolName: toolName,
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
referencedImageId: imageId,
|
||||
},
|
||||
data: {
|
||||
@@ -54,32 +51,17 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
|
||||
handles: {
|
||||
textBox: measurement.textBox ?? {},
|
||||
},
|
||||
cachedStats: {
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
},
|
||||
frameNumber: frameNumber,
|
||||
cachedStats: {},
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
frameNumber,
|
||||
},
|
||||
};
|
||||
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
annotationManager.addAnnotation(SRAnnotation);
|
||||
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||
|
||||
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
||||
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
||||
// It'd be super weird if it didn't anyway as a SCOORD.
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frameNumber;
|
||||
delete measurement.coords;
|
||||
}
|
||||
|
||||
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
|
||||
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
|
||||
function _getRenderableData(GraphicType, GraphicData, imageId) {
|
||||
let renderableData: csTypes.Point3[];
|
||||
|
||||
switch (GraphicType) {
|
||||
+1
-1
@@ -43,7 +43,7 @@ const convertSites = (codingValues, sites) => {
|
||||
*
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
{ servicesManager, extensionManager, appConfig }: withAppTypes,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
@@ -1,19 +1,20 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ServicesManager, ExtensionManager } from '@ohif/core';
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
|
||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||
|
||||
function OHIFCornerstoneSRViewport(props) {
|
||||
function OHIFCornerstoneSRViewport(props: withAppTypes) {
|
||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
|
||||
@@ -125,6 +126,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
||||
}
|
||||
|
||||
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
_getViewportReferencedDisplaySetData(
|
||||
srDisplaySet,
|
||||
newMeasurementSelected,
|
||||
@@ -199,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// The positionIds for the viewport aren't meaningful for the child display sets
|
||||
positionIds: null,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
@@ -253,12 +261,16 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
* if it is hydrated we don't even use the SR viewport.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
}, [srDisplaySet]);
|
||||
const loadSR = async () => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
await srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
updateViewport(measurementSelected);
|
||||
};
|
||||
loadSR();
|
||||
}, [dataSource, srDisplaySet]);
|
||||
|
||||
/**
|
||||
* Hook to update the tracking identifiers when the selected measurement changes or
|
||||
@@ -275,19 +287,11 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||
* it is updating a local variable? which is not state.
|
||||
*/
|
||||
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
useEffect(() => {
|
||||
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
}, [trackedMeasurements]);
|
||||
|
||||
/**
|
||||
* Data fetching for the SR displaySet, which updates the measurements and
|
||||
* also gets the referenced image displaySet that SR is based on.
|
||||
*/
|
||||
useEffect(() => {
|
||||
updateViewport(measurementSelected);
|
||||
}, [dataSource, srDisplaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
@@ -365,7 +369,7 @@ OHIFCornerstoneSRViewport.propTypes = {
|
||||
viewportLabel: PropTypes.string,
|
||||
customProps: PropTypes.object,
|
||||
viewportOptions: PropTypes.object,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
@@ -382,6 +386,13 @@ async function _getViewportReferencedDisplaySetData(
|
||||
const measurement = measurements[measurementSelected];
|
||||
|
||||
const { displaySetInstanceUID } = measurement;
|
||||
if (!displaySet.keyImageDisplaySet) {
|
||||
// Create a new display set, and preserve a reference to it here,
|
||||
// so that it can be re-displayed and shown inside the SR viewport.
|
||||
// This is only for ease of redisplay - the display set is stored in the
|
||||
// usual manner in the display set service.
|
||||
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const webpack = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, '../');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
const ENTRY = {
|
||||
app: `${SRC_DIR}/index.ts`,
|
||||
};
|
||||
|
||||
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
|
||||
return merge(commonConfig, {
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'ohif-extension-cornerstone',
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `./dist/${outputName}.css`,
|
||||
chunkFilename: `./dist/${outputName}.css`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,223 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([2d4e049](https://github.com/OHIF/Viewers/commit/2d4e049d8a54ca25c9dc02f29f2e53a6968d82e1))
|
||||
* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([9f5165d](https://github.com/OHIF/Viewers/commit/9f5165de47ed547919683498cfae87909de94786))
|
||||
* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([1b290bd](https://github.com/OHIF/Viewers/commit/1b290bdaa1a0ba750a4e5f539a51a636de529240))
|
||||
* **us:** logic for color conversion ([b7ebb0a](https://github.com/OHIF/Viewers/commit/b7ebb0aa8f814427045b7d5e7568dc0298ab0973))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
|
||||
@@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 cornerstone-dynamic-volume ()
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
# cornerstone-dynamic-volume
|
||||
## Description
|
||||
|
||||
## Author
|
||||
OHIF
|
||||
|
||||
## License
|
||||
MIT
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('../../babel.config.js');
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.8.1",
|
||||
"description": "OHIF extension for 4D volumes data",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
|
||||
"module": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./types": "./src/types/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.1\n",
|
||||
"@ohif/extension-cornerstone": "3.8.1\n",
|
||||
"@ohif/extension-default": "3.8.1\n",
|
||||
"@ohif/i18n": "3.8.1\n",
|
||||
"@ohif/ui": "3.8.1\n",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.77.6",
|
||||
"@cornerstonejs/tools": "^1.77.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
|
||||
|
||||
export { updateSegmentationsChartDisplaySet };
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
|
||||
import * as cs from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
|
||||
const CHART_MODALITY = 'CHT';
|
||||
const SEG_CHART_INSTANCE_UID = utils.guid();
|
||||
|
||||
// Private SOPClassUid for chart data
|
||||
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
|
||||
|
||||
const { utilities: csToolsUtils } = csTools;
|
||||
|
||||
function _getDateTimeStr() {
|
||||
const now = new Date();
|
||||
const date =
|
||||
now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2);
|
||||
const time =
|
||||
('0' + now.getUTCHours()).slice(-2) +
|
||||
('0' + now.getUTCMinutes()).slice(-2) +
|
||||
('0' + now.getUTCSeconds()).slice(-2);
|
||||
|
||||
return { date, time };
|
||||
}
|
||||
|
||||
function _getTimePointsDataByTagName(volume, timePointsTag) {
|
||||
const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => {
|
||||
const instance = DicomMetadataStore.getInstanceByImageId(imageId);
|
||||
const timePointValue = instance[timePointsTag];
|
||||
|
||||
if (timePointValue !== undefined) {
|
||||
timePoints.add(timePointValue);
|
||||
}
|
||||
|
||||
return timePoints;
|
||||
}, new Set());
|
||||
|
||||
return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b);
|
||||
}
|
||||
|
||||
function _convertTimePointsUnit(timePoints, timePointsUnit) {
|
||||
const validUnits = ['ms', 's', 'm', 'h'];
|
||||
const divisors = [1000, 60, 60];
|
||||
const currentUnitIndex = validUnits.indexOf(timePointsUnit);
|
||||
let divisor = 1;
|
||||
|
||||
if (currentUnitIndex !== -1) {
|
||||
for (let i = currentUnitIndex; i < validUnits.length - 1; i++) {
|
||||
const newDivisor = divisor * divisors[i];
|
||||
const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length;
|
||||
|
||||
// Change the scale only if more than 50% of the time points are
|
||||
// greater than the new divisor.
|
||||
if (greaterThanDivisorCount <= timePoints.length / 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
divisor = newDivisor;
|
||||
timePointsUnit = validUnits[i + 1];
|
||||
}
|
||||
|
||||
if (divisor > 1) {
|
||||
timePoints = timePoints.map(timePoint => timePoint / divisor);
|
||||
}
|
||||
}
|
||||
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
// It currently supports only one tag but a few other will be added soon
|
||||
// Supported 4D Tags
|
||||
// (0018,1060) Trigger Time [NOK]
|
||||
// (0018,0081) Echo Time [NOK]
|
||||
// (0018,0086) Echo Number [NOK]
|
||||
// (0020,0100) Temporal Position Identifier [NOK]
|
||||
// (0054,1300) FrameReferenceTime [OK]
|
||||
function _getTimePointsData(volume) {
|
||||
const timePointsTags = {
|
||||
FrameReferenceTime: {
|
||||
unit: 'ms',
|
||||
},
|
||||
};
|
||||
|
||||
const timePointsTagNames = Object.keys(timePointsTags);
|
||||
let timePoints;
|
||||
let timePointsUnit;
|
||||
|
||||
for (let i = 0; i < timePointsTagNames.length; i++) {
|
||||
const tagName = timePointsTagNames[i];
|
||||
const curTimePoints = _getTimePointsDataByTagName(volume, tagName);
|
||||
|
||||
if (curTimePoints.length) {
|
||||
timePoints = curTimePoints;
|
||||
timePointsUnit = timePointsTags[tagName].unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!timePoints.length) {
|
||||
const concatTagNames = timePointsTagNames.join(', ');
|
||||
|
||||
throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`);
|
||||
}
|
||||
|
||||
const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit);
|
||||
|
||||
timePoints = convertedTimePoints.timePoints;
|
||||
timePointsUnit = convertedTimePoints.timePointsUnit;
|
||||
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
const anInstance = displaySet.instances?.[0];
|
||||
|
||||
if (anInstance) {
|
||||
return (
|
||||
anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID);
|
||||
let volumeCacheKey: string | undefined;
|
||||
const volumeId = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||
|
||||
for (const [key] of cs.cache._volumeCache) {
|
||||
if (key.includes(volumeId)) {
|
||||
volumeCacheKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let referencedDynamicVolume;
|
||||
if (volumeCacheKey) {
|
||||
referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey);
|
||||
}
|
||||
|
||||
const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId(
|
||||
referencedDynamicVolume.imageIds[0]
|
||||
);
|
||||
|
||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
}) as number[][];
|
||||
|
||||
const pixelCount = timeData.length;
|
||||
|
||||
if (pixelCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// since we only use one segmentation representation per segmentationId
|
||||
// it is fine to pick the first one
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
|
||||
segmentation.id
|
||||
);
|
||||
|
||||
const segmentationRepresentationUID =
|
||||
segmentationRepresentations[0].segmentationRepresentationUID;
|
||||
|
||||
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
|
||||
segmentationRepresentationUID
|
||||
);
|
||||
|
||||
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
||||
toolGroupId,
|
||||
segmentationRepresentationUID,
|
||||
1 // segmentIndex
|
||||
);
|
||||
|
||||
const hexColor = cs.utilities.color.rgbToHex(...color);
|
||||
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
||||
|
||||
if (!timePointsData) {
|
||||
timePointsData = _getTimePointsData(referencedDynamicVolume);
|
||||
volumesTimePointsCache.set(referencedDynamicVolume, timePointsData);
|
||||
}
|
||||
|
||||
const { timePoints, timePointsUnit } = timePointsData;
|
||||
|
||||
if (timePoints.length !== timeData[0].length) {
|
||||
throw new Error('Invalid number of time points returned');
|
||||
}
|
||||
|
||||
const timepointsCount = timePoints.length;
|
||||
const chartSeriesData = new Array(timepointsCount);
|
||||
|
||||
for (let i = 0; i < timepointsCount; i++) {
|
||||
const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0);
|
||||
|
||||
chartSeriesData[i] = [timePoints[i], average];
|
||||
}
|
||||
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
StudyDescription,
|
||||
chartData: {
|
||||
series: {
|
||||
label: segmentation.label,
|
||||
points: chartSeriesData,
|
||||
color: hexColor,
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
label: `Time (${timePointsUnit})`,
|
||||
},
|
||||
y: {
|
||||
label: `Vl (Bq/ml)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumesTimePointsCache = new WeakMap();
|
||||
const segmentationsData = segmentations.map(segmentation =>
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
||||
);
|
||||
|
||||
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||
const series = segmentationsData.reduce((allSeries, curSegData) => {
|
||||
return [...allSeries, curSegData.chartData.series];
|
||||
}, []);
|
||||
|
||||
const instance = {
|
||||
SOPClassUID: ChartDataSOPClassUid,
|
||||
Modality: CHART_MODALITY,
|
||||
SOPInstanceUID: utils.guid(),
|
||||
SeriesDate: seriesDate,
|
||||
SeriesTime: seriesTime,
|
||||
SeriesInstanceUID: SEG_CHART_INSTANCE_UID,
|
||||
StudyInstanceUID: segmentationsData[0].StudyInstanceUID,
|
||||
StudyDescription: segmentationsData[0].StudyDescription,
|
||||
SeriesNumber: 100,
|
||||
SeriesDescription: 'Segmentation chart series data',
|
||||
chartData: {
|
||||
series,
|
||||
axis: { ...segmentationsData[0].chartData.axis },
|
||||
},
|
||||
};
|
||||
|
||||
const seriesMetadata = {
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
StudyDescription: instance.StudyDescription,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SeriesDescription: instance.SeriesDescription,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
Modality: instance.Modality,
|
||||
};
|
||||
|
||||
return { seriesMetadata, instance };
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
const { segmentationService, displaySetService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
||||
|
||||
if (seriesMetadata && instance) {
|
||||
// An event is triggered after adding the instance and the displaySet is created
|
||||
DicomMetadataStore.addSeriesMetadata([seriesMetadata], true);
|
||||
DicomMetadataStore.addInstances([instance], true);
|
||||
}
|
||||
}
|
||||
|
||||
export { updateSegmentationsChartDisplaySet as default };
|
||||
@@ -0,0 +1,407 @@
|
||||
import * as importedActions from './actions';
|
||||
import { utilities, Enums } from '@cornerstonejs/tools';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
const services = servicesManager.services;
|
||||
const { displaySetService, viewportGridService, segmentationService } = services;
|
||||
|
||||
const actions = {
|
||||
...importedActions,
|
||||
getDynamic4DDisplaySet: () => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
const anInstance = displaySet.instances?.[0];
|
||||
|
||||
if (anInstance) {
|
||||
return (
|
||||
anInstance.FrameReferenceTime !== undefined ||
|
||||
anInstance.NumberOfTimeSlices !== undefined ||
|
||||
anInstance.TemporalPositionIdentifier !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return dynamic4DDisplaySet;
|
||||
},
|
||||
getComputedDisplaySets: () => {
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySets = [...displaySetCache.values()];
|
||||
const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
|
||||
return displaySet.isDerived;
|
||||
});
|
||||
return computedDisplaySets;
|
||||
},
|
||||
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
|
||||
|
||||
// cache._volumeCache is a map that has a key that includes the volumeId
|
||||
// it is not exactly the volumeId, but it is the key that includes the volumeId
|
||||
// so we can't do cache._volumeCache.get(volumeId) we should iterate
|
||||
// over the keys and find the one that includes the volumeId
|
||||
let volumeCacheKey: string | undefined;
|
||||
|
||||
for (const [key] of cache._volumeCache) {
|
||||
if (key.includes(volumeId)) {
|
||||
volumeCacheKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let dynamicVolume;
|
||||
if (volumeCacheKey) {
|
||||
dynamicVolume = cache.getVolume(volumeCacheKey);
|
||||
}
|
||||
|
||||
const instance = dynamic4DDisplaySet.instances[0];
|
||||
|
||||
const csv = [];
|
||||
|
||||
// CSV header information with placeholder empty values for the metadata lines
|
||||
csv.push(`Patient ID,${instance.PatientID},`);
|
||||
csv.push(`Study Date,${instance.StudyDate},`);
|
||||
csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
|
||||
csv.push(`StudyDescription,${instance.StudyDescription},`);
|
||||
csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);
|
||||
|
||||
// empty line
|
||||
csv.push('');
|
||||
csv.push('');
|
||||
|
||||
// Helper function to calculate standard deviation
|
||||
function calculateStandardDeviation(data) {
|
||||
const n = data.length;
|
||||
const mean = data.reduce((acc, value) => acc + value, 0) / n;
|
||||
const squaredDifferences = data.map(value => (value - mean) ** 2);
|
||||
const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
|
||||
const stdDeviation = Math.sqrt(variance);
|
||||
return stdDeviation;
|
||||
}
|
||||
|
||||
// Iterate through each segmentation to get the timeData and ijkCoords
|
||||
segmentations.forEach((segmentation, segmentationIndex) => {
|
||||
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
}) as number[][];
|
||||
|
||||
if (summaryStats) {
|
||||
// Adding column headers for pixel identifier and segmentation label ids
|
||||
let headers = 'Operation,Segmentation Label ID';
|
||||
const maxLength = dynamicVolume.numTimePoints;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
headers += `,Time Point ${t}`;
|
||||
}
|
||||
csv.push(headers);
|
||||
// // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
|
||||
// // all the voxels in the ROI
|
||||
const mean = [];
|
||||
const min = [];
|
||||
const minIJK = [];
|
||||
const max = [];
|
||||
const maxIJK = [];
|
||||
const std = [];
|
||||
|
||||
const numVoxels = timeData.length;
|
||||
// Helper function to calculate standard deviation
|
||||
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||
// for each voxel in the ROI, get the value at the current time point
|
||||
const voxelValues = [];
|
||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||
voxelValues.push(timeData[voxelIndex][timeIndex]);
|
||||
}
|
||||
|
||||
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
|
||||
const minimum = Math.min(...voxelValues);
|
||||
min.push(minimum);
|
||||
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
||||
const maximum = Math.max(...voxelValues);
|
||||
max.push(maximum);
|
||||
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
||||
std.push(calculateStandardDeviation(voxelValues));
|
||||
}
|
||||
|
||||
let row = `Mean,${segmentation.label}`;
|
||||
// Generate separate rows for each statistic
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${mean[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Standard Deviation,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${std[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Min,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${min[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Max,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${max[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
} else {
|
||||
// Adding column headers for pixel identifier and segmentation label ids
|
||||
let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
|
||||
const maxLength = dynamicVolume.numTimePoints;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
headers += `,Time Point ${t}`;
|
||||
}
|
||||
csv.push(headers);
|
||||
// Assuming timeData and ijkCoords are of the same length
|
||||
for (let i = 0; i < timeData.length; i++) {
|
||||
// Generate the pixel identifier
|
||||
const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;
|
||||
|
||||
// Start a new row for the current pixel
|
||||
let row = `${pixelIdentifier},${segmentation.label}`;
|
||||
|
||||
// Add time data points for this pixel
|
||||
for (let t = 0; t < timeData[i].length; t++) {
|
||||
row += `,${timeData[i][t]}`;
|
||||
}
|
||||
|
||||
// Append the row to the CSV array
|
||||
csv.push(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to CSV string
|
||||
const csvContent = csv.join('\n');
|
||||
|
||||
// Generate filename and trigger download
|
||||
const filename = `${instance.PatientID}.csv`;
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
|
||||
const computedDisplaySet = displaySet;
|
||||
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySetKeys = [displaySetCache.keys()];
|
||||
const { displaySetInstanceUID } = computedDisplaySet;
|
||||
// Check to see if computed display set is already in cache
|
||||
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||
displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
|
||||
}
|
||||
|
||||
// Get all viewports and their corresponding indices
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// get the viewports in the grid
|
||||
// iterate over them and find the ones that are showing a dynamic
|
||||
// volume (displaySet), and replace that exact displaySet with the
|
||||
// computed displaySet
|
||||
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const viewportsToUpdate = [];
|
||||
|
||||
for (const [key, value] of viewports) {
|
||||
const viewport = value;
|
||||
const viewportOptions = viewport.viewportOptions;
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||
dynamic4DDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (displaySetInstanceUIDIndex !== -1) {
|
||||
const newViewport = {
|
||||
viewportId: viewport.viewportId,
|
||||
// merge the other displaySetInstanceUIDs with the new one
|
||||
displaySetInstanceUIDs: [
|
||||
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||
displaySetInstanceUID,
|
||||
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||
],
|
||||
viewportOptions: {
|
||||
initialImageOptions: viewportOptions.initialImageOptions,
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
background: viewportOptions.background,
|
||||
},
|
||||
};
|
||||
viewportsToUpdate.push(newViewport);
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: () => {
|
||||
// Todo: this assumes there is only one dynamic display set in the viewer
|
||||
const dynamicDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
|
||||
const { displaySetInstanceUID } = dynamicDisplaySet;
|
||||
|
||||
// Check to see if dynamic display set is already in cache
|
||||
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||
displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
|
||||
}
|
||||
|
||||
// Get all viewports and their corresponding indices
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// Get the computed 4D display set
|
||||
const computed4DDisplaySet = actions.getComputedDisplaySets()[0];
|
||||
|
||||
const viewportsToUpdate = [];
|
||||
|
||||
for (const [key, value] of viewports) {
|
||||
const viewport = value;
|
||||
const viewportOptions = viewport.viewportOptions;
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||
computed4DDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (displaySetInstanceUIDIndex !== -1) {
|
||||
const newViewport = {
|
||||
viewportId: viewport.viewportId,
|
||||
// merge the other displaySetInstanceUIDs with the new one
|
||||
displaySetInstanceUIDs: [
|
||||
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||
displaySetInstanceUID,
|
||||
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||
],
|
||||
viewportOptions: {
|
||||
initialImageOptions: viewportOptions.initialImageOptions,
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
background: viewportOptions.background,
|
||||
},
|
||||
};
|
||||
viewportsToUpdate.push(newViewport);
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: async ({ label }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
|
||||
// get the dynamic 4D display set
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// check if the dynamic 4D display set is in the display, if not we might have
|
||||
// the computed volumes and we should choose them for the segmentation
|
||||
// creation
|
||||
|
||||
let referenceDisplaySet;
|
||||
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
|
||||
const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
|
||||
dynamic4DDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (dynamicIsInActiveViewport) {
|
||||
referenceDisplaySet = dynamic4DDisplaySet;
|
||||
}
|
||||
|
||||
if (!referenceDisplaySet) {
|
||||
// try to see if there is any derived displaySet in the active viewport
|
||||
// which is referencing the dynamic 4D display set
|
||||
|
||||
// Todo: this is wrong but I don't have time to fix it now
|
||||
const cachedDisplaySets = displaySetService.getDisplaySetCache();
|
||||
for (const [key, displaySet] of cachedDisplaySets) {
|
||||
if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
|
||||
referenceDisplaySet = displaySet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!referenceDisplaySet) {
|
||||
throw new Error('No reference display set found based on the dynamic data');
|
||||
}
|
||||
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
referenceDisplaySet.displaySetInstanceUID,
|
||||
{ label }
|
||||
);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = Array.from(
|
||||
viewports.values(),
|
||||
viewport => viewport.viewportOptions.toolGroupId
|
||||
);
|
||||
|
||||
const representationType = LABELMAP;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const hydrateSegmentation = true;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
}
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
updateSegmentationsChartDisplaySet: {
|
||||
commandFn: actions.updateSegmentationsChartDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
exportTimeReportCSV: {
|
||||
commandFn: actions.exportTimeReportCSV,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: {
|
||||
commandFn: actions.swapDynamicWithComputedDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: {
|
||||
commandFn: actions.createNewLabelMapForDynamicVolume,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: {
|
||||
commandFn: actions.swapComputedWithDynamicDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -0,0 +1,654 @@
|
||||
const DEFAULT_COLORMAP = '2hot';
|
||||
const toolGroupIds = {
|
||||
pt: 'dynamic4D-pt',
|
||||
fusion: 'dynamic4D-fusion',
|
||||
ct: 'dynamic4D-ct',
|
||||
};
|
||||
|
||||
function getPTOptions({
|
||||
colormap,
|
||||
voiInverted,
|
||||
}: {
|
||||
colormap?: {
|
||||
name: string;
|
||||
opacity:
|
||||
| number
|
||||
| {
|
||||
value: number;
|
||||
opacity: number;
|
||||
}[];
|
||||
};
|
||||
voiInverted?: boolean;
|
||||
} = {}) {
|
||||
return {
|
||||
blendMode: 'MIP',
|
||||
colormap,
|
||||
voi: {
|
||||
windowWidth: 5,
|
||||
windowCenter: 2.5,
|
||||
},
|
||||
voiInverted,
|
||||
};
|
||||
}
|
||||
|
||||
function getPTViewports() {
|
||||
const ptOptionsParams = {
|
||||
colormap: {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 1 },
|
||||
{ value: 1, opacity: 1 },
|
||||
],
|
||||
},
|
||||
voiInverted: false,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getFusionViewports() {
|
||||
const ptOptionsParams = {
|
||||
colormap: {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.3 },
|
||||
{ value: 1, opacity: 0.3 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getSeriesChartViewport() {
|
||||
return {
|
||||
viewportOptions: {
|
||||
viewportId: 'seriesChart',
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'chartDisplaySet',
|
||||
options: {
|
||||
// This dataset does not require the download of any instance since it is pre-computed locally,
|
||||
// but interleaveTopToBottom.ts was not loading any series because it consider that all viewports
|
||||
// are a Cornerstone viewport which is not true in this case and it waits for all viewports to
|
||||
// have called interleaveTopToBottom(...).
|
||||
skipLoading: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getCTViewports() {
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const defaultProtocol = {
|
||||
id: 'default4D',
|
||||
locked: true,
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
modifiedDate: '2023-01-01T00:00:00.000Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
imageLoadStrategy: 'default', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
attribute: 'ModalitiesInStudy',
|
||||
constraint: {
|
||||
contains: ['CT', 'PT'],
|
||||
},
|
||||
},
|
||||
],
|
||||
// -1 would be used to indicate active only, whereas other values are
|
||||
// the number of required priors referenced - so 0 means active with
|
||||
// 0 or more priors.
|
||||
numberOfPriorsReferenced: -1,
|
||||
displaySetSelectors: {
|
||||
defaultDisplaySetId: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
// Try to match series with images by default, to prevent weird display
|
||||
// on SEG/SR containing studies
|
||||
{
|
||||
attribute: 'numImageFrames',
|
||||
constraint: {
|
||||
greaterThan: { value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
ctDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: 'CT',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
ptDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: 'PT',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
contains: 'Corrected',
|
||||
},
|
||||
},
|
||||
{
|
||||
weight: 2,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
doesNotContain: {
|
||||
value: 'Uncorrected',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Should we check if CorrectedImage contains ATTN?
|
||||
// (0028,0051) (CorrectedImage): NORM\DTIM\ATTN\SCAT\RADL\DECY
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
chartDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: 'CHT',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'dataPreparation',
|
||||
name: 'Data Preparation',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getPTViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'registration',
|
||||
name: 'Registration',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports(), ...getCTViewports(), ...getPTViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'roiQuantification',
|
||||
name: 'ROI Quantification',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'kineticAnalysis',
|
||||
name: 'Kinetic Analysis',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 2,
|
||||
columns: 3,
|
||||
layoutOptions: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 1 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 2 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 1 / 2,
|
||||
width: 1,
|
||||
height: 1 / 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports(), getSeriesChartViewport()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* HangingProtocolModule should provide a list of hanging protocols that will be
|
||||
* available in OHIF for Modes to use to decide on the structure of the viewports
|
||||
* and also the series that hung in the viewports. Each hanging protocol is defined by
|
||||
* { name, protocols}. Examples include the default hanging protocol provided by
|
||||
* the default extension that shows 2x2 viewports.
|
||||
*/
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: defaultProtocol.id,
|
||||
protocol: defaultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getHangingProtocolModule;
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicToolbox = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicExport = () => {
|
||||
return (
|
||||
<>
|
||||
<DynamicExport
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dynamic-volume',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicDataPanel,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-toolbox',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: 'Dynamic Toolbox',
|
||||
component: wrappedDynamicToolbox,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-export',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicExport,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getPanelModule;
|
||||
@@ -0,0 +1,6 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dynamic-volume';
|
||||
|
||||
export { id, SOPClassHandlerName };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { id } from './id';
|
||||
import commandsModule from './commandsModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
const dynamicVolumeExtension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
|
||||
/**
|
||||
* Perform any pre-registration tasks here. This is called before the extension
|
||||
* is registered. Usually we run tasks such as: configuring the libraries
|
||||
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
|
||||
* this extension is providing.
|
||||
*/
|
||||
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {
|
||||
// TODO: look for the right fix
|
||||
cache.setMaxCacheSize(5 * 1024 * 1024 * 1024);
|
||||
},
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||
*/
|
||||
getPanelModule,
|
||||
/**
|
||||
* ViewportModule should provide a list of viewports that will be available in OHIF
|
||||
* for Modes to consume and use in the viewports. Each viewport is defined by
|
||||
* {name, component} object. Example of a viewport module is the CornerstoneViewport
|
||||
* that is provided by the Cornerstone extension in OHIF.
|
||||
*/
|
||||
getHangingProtocolModule,
|
||||
/**
|
||||
* CommandsModule should provide a list of commands that will be available in OHIF
|
||||
* for Modes to consume and use in the viewports. Each command is defined by
|
||||
* an object of { actions, definitions, defaultContext } where actions is an
|
||||
* object of functions, definitions is an object of available commands, their
|
||||
* options, and defaultContext is the default context for the command to run against.
|
||||
*/
|
||||
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export { dynamicVolumeExtension as default };
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
|
||||
function DynamicDataPanel({ servicesManager, commandsManager }) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col text-white"
|
||||
data-cy={'dynamic-volume-panel'}
|
||||
>
|
||||
<PanelGenerateImage
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
></PanelGenerateImage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicDataPanel;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { t } = useTranslation('dynamicExport');
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export Time Data',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
{
|
||||
label: 'Export ROI Stats',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-3 flex justify-center px-2">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicExport;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
InputDoubleRange,
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
IconButton,
|
||||
InputNumber,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
sizeClassName: 'w-[58px] h-[28px]',
|
||||
arrowsDirection: 'horizontal',
|
||||
labelPosition: 'bottom',
|
||||
};
|
||||
|
||||
const Header = ({ title, tooltip }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Tooltip
|
||||
content={<div className="text-white">{tooltip}</div>}
|
||||
position="bottom-left"
|
||||
tight={true}
|
||||
tooltipBoxClassName="max-w-xs p-2"
|
||||
>
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="text-primary-active h-[14px] w-[14px]"
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DynamicVolumeControls = ({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
// fps
|
||||
fps,
|
||||
onFpsChange,
|
||||
minFps,
|
||||
maxFps,
|
||||
// Frames
|
||||
currentFrameIndex,
|
||||
onFrameChange,
|
||||
framesLength,
|
||||
onGenerate,
|
||||
onDoubleRangeChange,
|
||||
onDynamicClick,
|
||||
}) => {
|
||||
const [computedView, setComputedView] = useState(false);
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
||||
|
||||
useEffect(() => {
|
||||
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
||||
}, [framesLength]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
|
||||
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
||||
return;
|
||||
}
|
||||
setSliderRangeValues(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
title="Controls"
|
||||
childrenClassName="space-y-4 pb-5 px-5"
|
||||
>
|
||||
<div className="mt-2">
|
||||
<Header
|
||||
title="View"
|
||||
tooltip={
|
||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||
}
|
||||
/>
|
||||
<ButtonGroup className="mt-2 w-full">
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(false);
|
||||
onDynamicClick?.();
|
||||
}}
|
||||
>
|
||||
4D
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(true);
|
||||
}}
|
||||
>
|
||||
Computed
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FrameControls
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
isPlaying={isPlaying}
|
||||
computedView={computedView}
|
||||
// fps
|
||||
fps={fps}
|
||||
onFpsChange={onFpsChange}
|
||||
minFps={minFps}
|
||||
maxFps={maxFps}
|
||||
//
|
||||
framesLength={framesLength}
|
||||
onFrameChange={onFrameChange}
|
||||
currentFrameIndex={currentFrameIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||
<Header
|
||||
title="Computed Operation"
|
||||
tooltip={
|
||||
<div>
|
||||
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
|
||||
applied to the data set.
|
||||
<br></br> Range Slider: Choose the numeric range within which the operation will be
|
||||
performed.
|
||||
<br></br>Generate Button: Execute the chosen operation on the specified range of
|
||||
data.{' '}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full `}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="w-ful mt-2">
|
||||
<InputDoubleRange
|
||||
values={sliderRangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={0}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
maxValue={framesLength}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
||||
onClick={() => {
|
||||
onGenerate(computeViewMode);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicVolumeControls;
|
||||
|
||||
function FrameControls({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
fps,
|
||||
minFps,
|
||||
maxFps,
|
||||
onFpsChange,
|
||||
framesLength,
|
||||
onFrameChange,
|
||||
currentFrameIndex,
|
||||
computedView,
|
||||
}) {
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
return (
|
||||
<div className={computedView && 'ohif-disabled'}>
|
||||
<Header
|
||||
title="4D Controls"
|
||||
tooltip={
|
||||
<div>
|
||||
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br></br> Frame
|
||||
Selector: Navigate through individual frames of the 4D data. <br></br> FPS (Frames Per
|
||||
Second) Selector: Adjust the playback speed of the animation.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="mt-3 flex justify-between">
|
||||
<IconButton
|
||||
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
<InputNumber
|
||||
value={fps}
|
||||
onChange={onFpsChange}
|
||||
minValue={minFps}
|
||||
maxValue={maxFps}
|
||||
{...controlClassNames}
|
||||
label="FPS"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { InputDoubleRange } from '@ohif/ui';
|
||||
import { Select } from '@ohif/ui';
|
||||
import { Button } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const GenerateVolume = ({
|
||||
rangeValues,
|
||||
handleSliderChange,
|
||||
operationsUI,
|
||||
options,
|
||||
handleGenerateOptionsChange,
|
||||
onGenerateImage,
|
||||
returnTo4D,
|
||||
displayingComputedVolume,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2 text-white">Computed Image</div>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mr-2 bg-black text-white "
|
||||
options={operationsUI}
|
||||
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
|
||||
value={options.Operation}
|
||||
onChange={({ value }) => {
|
||||
handleGenerateOptionsChange({
|
||||
Operation: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputDoubleRange
|
||||
values={rangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={rangeValues[0] || 1}
|
||||
maxValue={rangeValues[1] || 2}
|
||||
showLabel={true}
|
||||
step={1}
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={onGenerateImage}
|
||||
className="w-1/2"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
<Button
|
||||
onClick={returnTo4D}
|
||||
disabled={!displayingComputedVolume}
|
||||
className="w-1/2"
|
||||
>
|
||||
Return To 4D
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GenerateVolume.propTypes = {
|
||||
rangeValues: PropTypes.array.isRequired,
|
||||
handleSliderChange: PropTypes.func.isRequired,
|
||||
operationsUI: PropTypes.array.isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
handleGenerateOptionsChange: PropTypes.func.isRequired,
|
||||
onGenerateImage: PropTypes.func.isRequired,
|
||||
returnTo4D: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GenerateVolume;
|
||||
@@ -0,0 +1,275 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
|
||||
|
||||
export default function PanelGenerateImage({ servicesManager, commandsManager }: withAppTypes) {
|
||||
const { cornerstoneViewportService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const [{ isCineEnabled }, cineService] = useCine();
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
|
||||
const [timePointRendered, setTimePointRendered] = useState(null);
|
||||
const [displayingComputed, setDisplayingComputed] = useState(false);
|
||||
|
||||
//
|
||||
const uuidComputedVolume = useRef(csUtils.uuidv4());
|
||||
const uuidDynamicVolume = useRef(null);
|
||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||
|
||||
useEffect(() => {
|
||||
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
|
||||
evtDetails.viewportData.data.forEach(volumeData => {
|
||||
if (volumeData.volume.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||
setTimePointsRange([1, volumeData.volume.numTimePoints]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
||||
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
|
||||
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetUID =>
|
||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||
);
|
||||
|
||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||
|
||||
if (!dynamicVolumeDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicVolume = cache
|
||||
.getVolumes()
|
||||
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
|
||||
|
||||
if (!dynamicVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
||||
}, [activeViewportId, cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
function renderGeneratedImage(displaySet) {
|
||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||
displaySet,
|
||||
});
|
||||
|
||||
setDisplayingComputed(true);
|
||||
}
|
||||
|
||||
function renderDynamicImage(displaySet) {
|
||||
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
|
||||
}
|
||||
|
||||
// Get computed volume from cache, calculate the data across the time frames,
|
||||
// set the scalar data to the computedVolume, and create displaySet
|
||||
async function onGenerateImage(operationName) {
|
||||
const dynamicVolumeId = dynamicVolume.volumeId;
|
||||
|
||||
if (!dynamicVolumeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let computedVolume = cache.getVolume(computedVolumeId);
|
||||
|
||||
if (!computedVolume) {
|
||||
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
||||
computedVolume = cache.getVolume(computedVolumeId);
|
||||
}
|
||||
|
||||
const vals = timePointsRangeToUseForGenerate;
|
||||
|
||||
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
|
||||
|
||||
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
|
||||
dynamicVolume,
|
||||
operationName,
|
||||
operationName === 'SUBTRACT' ? vals : targets
|
||||
);
|
||||
|
||||
// Add loadStatus.loaded to computed volume and set to true
|
||||
computedVolume.loadStatus = {};
|
||||
computedVolume.loadStatus.loaded = true;
|
||||
// Set computed scalar data to volume
|
||||
const scalarData = computedVolume.getScalarData();
|
||||
scalarData.set(dataInTime);
|
||||
|
||||
// If computed display set does not exist, create an object to be used as
|
||||
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||
if (!computedDisplaySet) {
|
||||
const displaySet = {
|
||||
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
|
||||
displaySetInstanceUID: uuidComputedVolume.current,
|
||||
SOPClassHandlerId: SOPClassHandlerId,
|
||||
Modality: dynamicVolume.metadata.Modality,
|
||||
isMultiFrame: false,
|
||||
numImageFrames: 1,
|
||||
uid: uuidComputedVolume.current,
|
||||
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
|
||||
madeInClient: true,
|
||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||
isDerived: true,
|
||||
};
|
||||
setComputedDisplaySet(displaySet);
|
||||
renderGeneratedImage(displaySet);
|
||||
} else {
|
||||
commandsManager.runCommand('updateVolumeData', {
|
||||
volume: computedVolume,
|
||||
});
|
||||
// Check if viewport is currently displaying the computed volume, if so,
|
||||
// call render on the viewports to update the image, if not, call
|
||||
// renderGeneratedImage
|
||||
// if (!cache.getVolume(dynamicVolumeId)) {
|
||||
// for (const viewportId of viewports.keys()) {
|
||||
// const viewportForRendering =
|
||||
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
// viewportForRendering.render();
|
||||
// }
|
||||
// } else {
|
||||
cornerstoneViewportService.getRenderingEngine().render();
|
||||
renderGeneratedImage(computedDisplaySet);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const onPlayPauseChange = isPlaying => {
|
||||
isPlaying ? handlePlay() : handleStop();
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
setIsPlaying(true);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = viewportInfo;
|
||||
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
setIsPlaying(false);
|
||||
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
cineService.stopClip(element);
|
||||
};
|
||||
|
||||
const handleSetFrameRate = newFrameRate => {
|
||||
setFrameRate(newFrameRate);
|
||||
handleStop();
|
||||
handlePlay();
|
||||
};
|
||||
|
||||
function handleSliderChange(newValues) {
|
||||
if (
|
||||
newValues[0] === timePointsRangeToUseForGenerate[0] &&
|
||||
newValues[1] === timePointsRangeToUseForGenerate[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimePointsRangeToUseForGenerate(newValues);
|
||||
}
|
||||
|
||||
if (!dynamicVolume || timePointsRange.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicVolumeControls
|
||||
fps={frameRate}
|
||||
isPlaying={isPlaying}
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
minFps={1}
|
||||
maxFps={50}
|
||||
currentFrameIndex={timePointRendered}
|
||||
onFpsChange={handleSetFrameRate}
|
||||
framesLength={timePointsRange[1]}
|
||||
onFrameChange={timePointIndex => {
|
||||
dynamicVolume.timePointIndex = timePointIndex;
|
||||
}}
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
||||
if (!cache.getVolume(computedVolumeId)) {
|
||||
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
return computedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
PanelGenerateImage.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
measurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
function WorkflowPanel({ servicesManager, extensionManager }) {
|
||||
const ProgressDropdownWithService = useMemo(() => {
|
||||
const defaultComponents = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-default.customizationModule.default'
|
||||
).value;
|
||||
|
||||
return defaultComponents.find(
|
||||
component => component.id === 'progressDropdownWithServiceComponent'
|
||||
).component;
|
||||
}, [extensionManager]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy={'workflow-panel'}
|
||||
className="bg-secondary-dark mb-1 px-3 py-4"
|
||||
>
|
||||
<div className="mb-1">Workflow</div>
|
||||
<div>
|
||||
<ProgressDropdownWithService servicesManager={servicesManager} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowPanel;
|
||||
@@ -0,0 +1,5 @@
|
||||
import DynamicDataPanel from './DynamicDataPanel';
|
||||
import WorkflowPanel from './WorkflowPanel';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
|
||||
export { DynamicDataPanel, WorkflowPanel, PanelGenerateImage };
|
||||
@@ -3,6 +3,260 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **contour:** set renderFill to false for contour ([#4186](https://github.com/OHIF/Viewers/issues/4186)) ([471e6a1](https://github.com/OHIF/Viewers/commit/471e6a1559d983267448058b8e17461f79130c79))
|
||||
* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([2d4e049](https://github.com/OHIF/Viewers/commit/2d4e049d8a54ca25c9dc02f29f2e53a6968d82e1))
|
||||
* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([9f5165d](https://github.com/OHIF/Viewers/commit/9f5165de47ed547919683498cfae87909de94786))
|
||||
* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([634abf3](https://github.com/OHIF/Viewers/commit/634abf353f02cffec462e341d594741fdefb6f22))
|
||||
* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([1b290bd](https://github.com/OHIF/Viewers/commit/1b290bdaa1a0ba750a4e5f539a51a636de529240))
|
||||
* **us:** logic for color conversion ([b7ebb0a](https://github.com/OHIF/Viewers/commit/b7ebb0aa8f814427045b7d5e7568dc0298ab0973))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport-sync:** Enable re-sync image slices in a different position when needed ([#3984](https://github.com/OHIF/Viewers/issues/3984)) ([6ebd2cc](https://github.com/OHIF/Viewers/commit/6ebd2cc7cb70cd88fd01dc1e516077f27b201943))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **vewport:** Add missing blendmodes from cornerstonejs ([#4055](https://github.com/OHIF/Viewers/issues/4055)) ([3ec7e51](https://github.com/OHIF/Viewers/commit/3ec7e512169a07506388902acb5b2c118093fa50))
|
||||
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hp:** Fails to display any layouts in the layout selector if first layout has multiple stages ([#4058](https://github.com/OHIF/Viewers/issues/4058)) ([f0ed3fd](https://github.com/OHIF/Viewers/commit/f0ed3fd7b99b0e4e00b261ceb9888ba94726719c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "3.8.0-beta.71",
|
||||
"version": "3.8.1",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -38,10 +38,10 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.67.0",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.77.6",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.8.0-beta.71",
|
||||
"@ohif/ui": "3.8.0-beta.71",
|
||||
"@ohif/core": "3.8.1\n",
|
||||
"@ohif/ui": "3.8.1\n",
|
||||
"dcmjs": "^0.29.12",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
@@ -55,11 +55,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.67.0",
|
||||
"@cornerstonejs/core": "^1.67.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.67.0",
|
||||
"@cornerstonejs/tools": "^1.67.0",
|
||||
"@kitware/vtk.js": "29.7.0",
|
||||
"@cornerstonejs/adapters": "^1.77.6",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.77.6",
|
||||
"@cornerstonejs/tools": "^1.77.6",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import { Notification, useViewportDialog, Types as UITypes, AllInOneMenu } from '@ohif/ui';
|
||||
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
|
||||
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
@@ -18,7 +18,6 @@ import { setEnabledElement } from '../state';
|
||||
import './OHIFCornerstoneViewport.css';
|
||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import CornerstoneServices from '../types/CornerstoneServices';
|
||||
import CinePlayer from '../components/CinePlayer';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
@@ -100,7 +99,7 @@ function areEqual(prevProps, nextProps) {
|
||||
|
||||
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||
// Then we don't need to worry about the re-renders if the props change.
|
||||
const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
const {
|
||||
displaySets,
|
||||
dataSource,
|
||||
@@ -108,16 +107,37 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
isJumpToMeasurementDisabled,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
// eslint-disable-next-line react/prop-types
|
||||
initialImageIndex,
|
||||
onReady,
|
||||
// if the viewport is part of a hanging protocol layout
|
||||
// we should not really rely on the old synchronizers and
|
||||
// you see below we only rehydrate the synchronizers if the viewport
|
||||
// is not part of the hanging protocol layout. HPs should
|
||||
// define their own synchronizers. Since the synchronizers are
|
||||
// viewportId dependent and
|
||||
// eslint-disable-next-line react/prop-types
|
||||
isHangingProtocolLayout,
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
if (!viewportId) {
|
||||
throw new Error('Viewport ID is required');
|
||||
}
|
||||
|
||||
// Since we only have support for dynamic data in volume viewports, we should
|
||||
// handle this case here and set the viewportType to volume if any of the
|
||||
// displaySets are dynamic volumes
|
||||
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume && ds.isReconstructable)
|
||||
? 'volume'
|
||||
: viewportOptions.viewportType;
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
const elementRef = useRef();
|
||||
@@ -134,8 +154,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
viewportActionCornersService,
|
||||
customizationService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
} = servicesManager.services;
|
||||
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
// useCallback for scroll bar height calculation
|
||||
@@ -188,14 +207,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
const synchronizersStore = stateSyncService.getState().synchronizersStore;
|
||||
|
||||
if (synchronizersStore?.[viewportId]?.length) {
|
||||
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
||||
// If the viewport used to have a synchronizer, re apply it again
|
||||
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
|
||||
}
|
||||
|
||||
if (onElementEnabled) {
|
||||
onElementEnabled(evt);
|
||||
onReady?.(evt);
|
||||
}
|
||||
},
|
||||
[viewportId, onElementEnabled, toolGroupService]
|
||||
@@ -436,6 +454,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
actions={viewportDialogState.actions}
|
||||
onSubmit={viewportDialogState.onSubmit}
|
||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||
onKeyPress={viewportDialogState.onKeyPress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -468,7 +487,10 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
cornerstoneViewportService.getViewportIdToJump(
|
||||
jumpId,
|
||||
measurement.displaySetInstanceUID,
|
||||
{ referencedImageId: measurement.referencedImageId }
|
||||
{
|
||||
referencedImageId:
|
||||
measurement.referencedImageId || measurement.metadata?.referencedImageId,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
||||
@@ -577,6 +599,9 @@ function _jumpToMeasurement(
|
||||
i => i.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
// the index is reversed in the volume viewport
|
||||
// imageIdIndex = referencedDisplaySet.images.length - 1 - imageIdIndex;
|
||||
|
||||
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
|
||||
|
||||
// should compare abs for both planes since the direction can be flipped
|
||||
|
||||
@@ -5,7 +5,7 @@ import CustomizableViewportOverlay from './CustomizableViewportOverlay';
|
||||
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
||||
|
||||
function CornerstoneOverlays(props) {
|
||||
function CornerstoneOverlays(props: withAppTypes) {
|
||||
const { viewportId, element, scrollbarHeight, servicesManager } = props;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [imageSliceData, setImageSliceData] = useState({
|
||||
|
||||
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { InstanceMetadata } from '@ohif/core/src/types';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
@@ -19,7 +18,7 @@ interface OverlayItemProps {
|
||||
element: HTMLElement;
|
||||
viewportData: ViewportData;
|
||||
imageSliceData: ImageSliceData;
|
||||
servicesManager: ServicesManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
viewportId: string;
|
||||
instance: InstanceMetadata;
|
||||
customization: any;
|
||||
@@ -59,7 +58,7 @@ function CustomizableViewportOverlay({
|
||||
viewportData: ViewportData;
|
||||
imageSliceData: ImageSliceData;
|
||||
viewportId: string;
|
||||
servicesManager: ServicesManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}) {
|
||||
const { cornerstoneViewportService, customizationService, toolGroupService } =
|
||||
servicesManager.services;
|
||||
@@ -318,11 +317,11 @@ function CustomizableViewportOverlay({
|
||||
function _getViewportInstances(viewportData) {
|
||||
const imageIds = [];
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
imageIds.push(viewportData.data.imageIds[0]);
|
||||
imageIds.push(viewportData.data[0].imageIds[0]);
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const volumes = viewportData.data;
|
||||
volumes.forEach(volume => {
|
||||
if (!volume?.imageIds) {
|
||||
if (!volume?.imageIds || volume.imageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
imageIds.push(volume.imageIds[0]);
|
||||
@@ -358,7 +357,7 @@ const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneView
|
||||
};
|
||||
|
||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
const imageIds = viewportData.data.imageIds;
|
||||
const imageIds = viewportData.data[0].imageIds;
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import { Enums, Types, utilities } from '@cornerstonejs/core';
|
||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||
import { ImageScrollbar } from '@ohif/ui';
|
||||
import { ServicesManger } from '@ohif/core';
|
||||
|
||||
function CornerstoneImageScrollbar({
|
||||
viewportData,
|
||||
@@ -13,8 +12,8 @@ function CornerstoneImageScrollbar({
|
||||
setImageSliceData,
|
||||
scrollbarHeight,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { cineService, cornerstoneViewportService } = (servicesManager as ServicesManger).services;
|
||||
}: withAppTypes) {
|
||||
const { cineService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const onImageScrollbarChange = (imageIndex, viewportId) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
@@ -23,7 +22,7 @@ function CornerstoneImageScrollbar({
|
||||
|
||||
if (isCineEnabled) {
|
||||
// on image scrollbar change, stop the CINE if it is playing
|
||||
cineService.stopClip(element);
|
||||
cineService.stopClip(element, { viewportId });
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
}
|
||||
|
||||
@@ -49,7 +48,7 @@ function CornerstoneImageScrollbar({
|
||||
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices: viewportData.data.imageIds.length,
|
||||
numberOfSlices: viewportData.data[0].imageIds.length,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -79,7 +78,7 @@ function CornerstoneImageScrollbar({
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({
|
||||
imageIndex: newImageIdIndex,
|
||||
numberOfSlices: viewportData.data.imageIds.length,
|
||||
numberOfSlices: viewportData.data[0].imageIds.length,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ function ViewportOrientationMarkers({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
orientationMarkers = ['top', 'left'],
|
||||
}) {
|
||||
}: withAppTypes) {
|
||||
// Rotation is in degrees
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [flipHorizontal, setFlipHorizontal] = useState(false);
|
||||
@@ -67,7 +67,7 @@ function ViewportOrientationMarkers({
|
||||
let rowCosines, columnCosines;
|
||||
if (viewportData.viewportType === 'stack') {
|
||||
const imageIndex = imageSliceData.imageIndex;
|
||||
const imageId = viewportData.data.imageIds?.[imageIndex];
|
||||
const imageId = viewportData.data[0].imageIds?.[imageIndex];
|
||||
|
||||
// Workaround for below TODO stub
|
||||
if (!imageId) {
|
||||
|
||||
@@ -20,7 +20,6 @@ import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/c
|
||||
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import { CornerstoneServices } from './types';
|
||||
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||
|
||||
const toggleSyncFunctions = {
|
||||
@@ -44,13 +43,19 @@ function commandsModule({
|
||||
colorbarService,
|
||||
hangingProtocolService,
|
||||
syncGroupService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
} = servicesManager.services;
|
||||
|
||||
const { measurementServiceSource } = this;
|
||||
|
||||
function _getActiveViewportEnabledElement() {
|
||||
return getActiveViewportEnabledElement(viewportGridService);
|
||||
}
|
||||
|
||||
function _getActiveViewportToolGroupId() {
|
||||
const viewport = _getActiveViewportEnabledElement();
|
||||
return toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Generates the selector props for the context menu, specific to
|
||||
@@ -126,16 +131,12 @@ function commandsModule({
|
||||
? nearbyToolData
|
||||
: null;
|
||||
},
|
||||
|
||||
// Measurement tool commands:
|
||||
|
||||
/** Delete the given measurement */
|
||||
deleteMeasurement: ({ uid }) => {
|
||||
if (uid) {
|
||||
measurementServiceSource.remove(uid);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the measurement labelling input dialog and update the label
|
||||
* on the measurement with a response if not cancelled.
|
||||
@@ -279,16 +280,16 @@ function commandsModule({
|
||||
|
||||
actions.setViewportWindowLevel({ ...props, viewportId });
|
||||
},
|
||||
setToolEnabled: ({ toolName, toggle }) => {
|
||||
setToolEnabled: ({ toolName, toggle, toolGroupId }) => {
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
if (!viewports.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroup(null);
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId ?? null);
|
||||
|
||||
if (!toolGroup) {
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -297,14 +298,56 @@ function commandsModule({
|
||||
// Toggle the tool's state only if the toggle is true
|
||||
if (toggle) {
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
} else {
|
||||
toolGroup.setToolEnabled(toolName);
|
||||
}
|
||||
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
toggleEnabledDisabledToolbar({ value, itemId, toolGroupId }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
},
|
||||
toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId();
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsActive = [
|
||||
Enums.ToolModes.Active,
|
||||
Enums.ToolModes.Enabled,
|
||||
Enums.ToolModes.Passive,
|
||||
].includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
toolIsActive
|
||||
? toolGroup.setToolDisabled(toolName)
|
||||
: actions.setToolActive({ toolName, toolGroupId });
|
||||
|
||||
// we should set the previously active tool to active after we set the
|
||||
// current tool disabled
|
||||
if (toolIsActive) {
|
||||
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||
if (prevToolName !== toolName) {
|
||||
actions.setToolActive({ toolName: prevToolName, toolGroupId });
|
||||
}
|
||||
}
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [] }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
toolName = toolName || itemId || value;
|
||||
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
|
||||
@@ -371,6 +414,7 @@ function commandsModule({
|
||||
onClose: uiModalService.hide,
|
||||
cornerstoneViewportService,
|
||||
},
|
||||
containerDimensions: 'w-[70%] max-w-[900px]',
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -406,11 +450,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { flipHorizontal } = viewport.getCamera();
|
||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||
viewport.render();
|
||||
}
|
||||
const { flipHorizontal } = viewport.getCamera();
|
||||
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||
viewport.render();
|
||||
},
|
||||
flipViewportVertical: () => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
@@ -421,11 +463,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { flipVertical } = viewport.getCamera();
|
||||
viewport.setCamera({ flipVertical: !flipVertical });
|
||||
viewport.render();
|
||||
}
|
||||
const { flipVertical } = viewport.getCamera();
|
||||
viewport.setCamera({ flipVertical: !flipVertical });
|
||||
viewport.render();
|
||||
},
|
||||
invertViewport: ({ element }) => {
|
||||
let enabledElement;
|
||||
@@ -639,6 +679,17 @@ function commandsModule({
|
||||
storePresentation: ({ viewportId }) => {
|
||||
cornerstoneViewportService.storePresentation({ viewportId });
|
||||
},
|
||||
updateVolumeData: ({ volume }) => {
|
||||
// update vtkOpenGLTexture and imageData of computed volume
|
||||
const { imageData, vtkOpenGLTexture } = volume;
|
||||
const numSlices = imageData.getDimensions()[2];
|
||||
const slicesToUpdate = [...Array(numSlices).keys()];
|
||||
slicesToUpdate.forEach(i => {
|
||||
vtkOpenGLTexture.setUpdatedFrame(i);
|
||||
});
|
||||
imageData.modified();
|
||||
},
|
||||
|
||||
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
|
||||
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
const command = protocol.callbacks.onViewportDataInitialized;
|
||||
@@ -773,7 +824,7 @@ function commandsModule({
|
||||
}
|
||||
|
||||
crosshairInstances.forEach(ins => {
|
||||
ins.resetCrosshairs();
|
||||
ins?.resetCrosshairs();
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -928,6 +979,15 @@ function commandsModule({
|
||||
toggleSynchronizer: {
|
||||
commandFn: actions.toggleSynchronizer,
|
||||
},
|
||||
updateVolumeData: {
|
||||
commandFn: actions.updateVolumeData,
|
||||
},
|
||||
toggleEnabledDisabledToolbar: {
|
||||
commandFn: actions.toggleEnabledDisabledToolbar,
|
||||
},
|
||||
toggleActiveDisabledToolbar: {
|
||||
commandFn: actions.toggleActiveDisabledToolbar,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
|
||||
|
||||
const ActiveViewportWindowLevel = ({ servicesManager }: withAppTypes): ReactElement => {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { activeViewportId } = viewportGrid;
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeViewportId && (
|
||||
<ViewportWindowLevel
|
||||
servicesManager={servicesManager}
|
||||
viewportId={activeViewportId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActiveViewportWindowLevel.propTypes = {
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ActiveViewportWindowLevel;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ActiveViewportWindowLevel';
|
||||
@@ -1,73 +1,123 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { CinePlayer, useCine } from '@ohif/ui';
|
||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
function WrappedCinePlayer({
|
||||
enabledVPElement,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}: withAppTypes<{
|
||||
enabledVPElement: HTMLElement;
|
||||
viewportId: string;
|
||||
}>) {
|
||||
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
||||
const [{ isCineEnabled, cines }, api] = useCine();
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
|
||||
const [dynamicInfo, setDynamicInfo] = useState(null);
|
||||
const [appConfig] = useAppConfig();
|
||||
const isMountedRef = useRef(null);
|
||||
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
||||
if (!cines?.[viewportId] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const { isPlaying = false, frameRate = 24 } = cines[viewportId];
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
api.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
api.stopClip(enabledVPElement);
|
||||
}
|
||||
return isPlaying
|
||||
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate, viewportId })
|
||||
: cineService.stopClip(enabledVPElement);
|
||||
};
|
||||
|
||||
const newStackCineHandler = useCallback(() => {
|
||||
const newDisplaySetHandler = useCallback(() => {
|
||||
if (!enabledVPElement || !isCineEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const { displaySetInstanceUIDs } = viewports.get(viewportId);
|
||||
|
||||
let frameRate = 24;
|
||||
let isPlaying = cines[viewportId].isPlaying;
|
||||
let isPlaying = cines[viewportId]?.isPlaying || false;
|
||||
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (displaySet.FrameRate) {
|
||||
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
|
||||
// So a bit of math to get the actual frame rate.
|
||||
frameRate = Math.round(1000 / displaySet.FrameRate);
|
||||
isPlaying ||= !!appConfig.autoPlayCine;
|
||||
}
|
||||
|
||||
// check if the displaySet is dynamic and set the dynamic info
|
||||
if (displaySet.isDynamicVolume) {
|
||||
const { dynamicVolumeInfo } = displaySet;
|
||||
const numTimePoints = dynamicVolumeInfo.timePoints.length;
|
||||
const label = dynamicVolumeInfo.splittingTag;
|
||||
const timePointIndex = dynamicVolumeInfo.timePointIndex || 0;
|
||||
setDynamicInfo({
|
||||
volumeId: displaySet.displaySetInstanceUID,
|
||||
timePointIndex,
|
||||
numTimePoints,
|
||||
label,
|
||||
});
|
||||
} else {
|
||||
setDynamicInfo(null);
|
||||
}
|
||||
});
|
||||
|
||||
if (isPlaying) {
|
||||
api.setIsCineEnabled(isPlaying);
|
||||
cineService.setIsCineEnabled(isPlaying);
|
||||
}
|
||||
api.setCine({ id: viewportId, isPlaying, frameRate });
|
||||
cineService.setCine({ id: viewportId, isPlaying, frameRate });
|
||||
setNewStackFrameRate(frameRate);
|
||||
}, [displaySetService, viewportId, viewportGridService, cines]);
|
||||
}, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled, enabledVPElement]);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
||||
newDisplaySetHandler();
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
api.stopClip(enabledVPElement);
|
||||
api.setCine({ id: viewportId, isPlaying: false });
|
||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
||||
};
|
||||
}, [enabledVPElement, newStackCineHandler]);
|
||||
}, [isCineEnabled, newDisplaySetHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCineEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
}, [isCineEnabled, cineHandler, enabledVPElement]);
|
||||
|
||||
/**
|
||||
* Use effect for handling new display set
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
||||
// this doesn't makes sense that we are listening to this event on viewport element
|
||||
enabledVPElement.addEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
newDisplaySetHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
|
||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
||||
enabledVPElement.removeEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
newDisplaySetHandler
|
||||
);
|
||||
};
|
||||
}, [enabledVPElement, newDisplaySetHandler, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
|
||||
@@ -77,41 +127,118 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
api.stopClip(enabledVPElement);
|
||||
cineService.stopClip(enabledVPElement, { viewportId });
|
||||
};
|
||||
}, [cines, viewportId, enabledVPElement, cineHandler]);
|
||||
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
||||
|
||||
if (!isCineEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
const isPlaying = cine?.isPlaying || false;
|
||||
|
||||
return (
|
||||
isCineEnabled && (
|
||||
<CinePlayerComponent
|
||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||
frameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
onClose={() => {
|
||||
// also stop the clip
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
isPlaying: false,
|
||||
});
|
||||
api.setIsCineEnabled(false);
|
||||
}}
|
||||
onPlayPauseChange={isPlaying => {
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
isPlaying,
|
||||
});
|
||||
}}
|
||||
onFrameRateChange={frameRate =>
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
<RenderCinePlayer
|
||||
viewportId={viewportId}
|
||||
cineService={cineService}
|
||||
newStackFrameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
dynamicInfo={dynamicInfo}
|
||||
customizationService={customizationService}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCinePlayer({
|
||||
viewportId,
|
||||
cineService,
|
||||
newStackFrameRate,
|
||||
isPlaying,
|
||||
dynamicInfo: dynamicInfoProp,
|
||||
customizationService,
|
||||
}) {
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
|
||||
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
|
||||
|
||||
useEffect(() => {
|
||||
setDynamicInfo(dynamicInfoProp);
|
||||
}, [dynamicInfoProp]);
|
||||
|
||||
/**
|
||||
* Use effect for handling 4D time index changed
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dynamicInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTimePointIndexChange = evt => {
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = evt.detail;
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(
|
||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
);
|
||||
};
|
||||
}, [dynamicInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dynamicInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
||||
const volume = cache.getVolume(volumeId);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
}, []);
|
||||
|
||||
const updateDynamicInfo = useCallback(props => {
|
||||
const { volumeId, timePointIndex } = props;
|
||||
const volume = cache.getVolume(volumeId);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CinePlayerComponent
|
||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||
frameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
onClose={() => {
|
||||
// also stop the clip
|
||||
cineService.setCine({
|
||||
id: viewportId,
|
||||
isPlaying: false,
|
||||
});
|
||||
cineService.setIsCineEnabled(false);
|
||||
}}
|
||||
onPlayPauseChange={isPlaying => {
|
||||
cineService.setCine({
|
||||
id: viewportId,
|
||||
isPlaying,
|
||||
});
|
||||
}}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: viewportId,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
dynamicInfo={dynamicInfo}
|
||||
updateDynamicInfo={updateDynamicInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { PanelSection, WindowLevel } from '@ohif/ui';
|
||||
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
||||
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
|
||||
|
||||
const { Events } = Enums;
|
||||
|
||||
const ViewportWindowLevel = ({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
}: withAppTypes<{
|
||||
viewportId: string;
|
||||
}>): ReactElement => {
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [windowLevels, setWindowLevels] = useState([]);
|
||||
const [cachedHistograms, setCachedHistograms] = useState({});
|
||||
|
||||
/**
|
||||
* Looks for all viewports that has exactly all volumeIds passed as parameter.
|
||||
*/
|
||||
const getViewportsWithVolumeIds = useCallback(
|
||||
(volumeIds: string[]) => {
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
const viewports = renderingEngine.getVolumeViewports();
|
||||
|
||||
return viewports.filter(vp => {
|
||||
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
|
||||
|
||||
return (
|
||||
volumeIds.length === viewportVolumeIds.length &&
|
||||
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
||||
);
|
||||
});
|
||||
},
|
||||
[cornerstoneViewportService]
|
||||
);
|
||||
|
||||
const getNodeOpacity = (volumeActor, nodeIndex) => {
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
const nodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
|
||||
|
||||
return nodeValue[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the opacity applied to the PET volume is something like
|
||||
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
|
||||
* constant opacity value for all x's greater than 0.1
|
||||
*/
|
||||
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const modality = volume.metadata.Modality;
|
||||
|
||||
if (modality !== 'PT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
|
||||
// It must have at least two points (0 and 0.1)
|
||||
if (volumeOpacity.getSize() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const node1Value = [];
|
||||
const node2Value = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, node1Value);
|
||||
volumeOpacity.getNodeValue(1, node2Value);
|
||||
|
||||
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
|
||||
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedOpacity = node2Value[1];
|
||||
const opacitySize = volumeOpacity.getSize();
|
||||
const currentNodeValue = [];
|
||||
|
||||
// Any point after 0.1 must have the same opacity
|
||||
for (let i = 2; i < opacitySize; i++) {
|
||||
volumeOpacity.getNodeValue(i, currentNodeValue);
|
||||
|
||||
if (currentNodeValue[1] !== expectedOpacity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the opacity function has a constance opacity value for all x's
|
||||
*/
|
||||
const isVolumeWithConstantOpacity = volumeActor => {
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
const opacitySize = volumeOpacity.getSize();
|
||||
const firstNodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, firstNodeValue);
|
||||
|
||||
const firstNodeOpacity = firstNodeValue[1];
|
||||
|
||||
for (let i = 0; i < opacitySize; i++) {
|
||||
const currentNodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, currentNodeValue);
|
||||
|
||||
if (currentNodeValue[1] !== firstNodeOpacity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getVolumeOpacity = useCallback((viewport, volumeId) => {
|
||||
const volumeActor = viewport.getActor(volumeId).actor;
|
||||
|
||||
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
|
||||
// Get the opacity from the second node at 0.1
|
||||
return getNodeOpacity(volumeActor, 1);
|
||||
} else if (isVolumeWithConstantOpacity(volumeActor)) {
|
||||
return getNodeOpacity(volumeActor, 0);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const getWindowLevelsData = useCallback(
|
||||
async (viewportId: number) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
const volumeIds = viewport.getActors().map(actor => actor.uid);
|
||||
const viewportProperties = viewport.getProperties();
|
||||
const { voiRange } = viewportProperties;
|
||||
const viewportVoi = voiRange
|
||||
? {
|
||||
windowWidth: voiRange.upper - voiRange.lower,
|
||||
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const windowLevels = await Promise.all(
|
||||
volumeIds.map(async (volumeId, volumeIndex) => {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const opacity = getVolumeOpacity(viewport, volumeId);
|
||||
const { metadata, scaling } = volume;
|
||||
const modality = metadata.Modality;
|
||||
|
||||
// TODO: find a proper way to fix the histogram
|
||||
const options = {
|
||||
min: modality === 'PT' ? 0.1 : -999,
|
||||
max: modality === 'PT' ? 5 : 10000,
|
||||
};
|
||||
|
||||
const histogram =
|
||||
cachedHistograms[volumeId] ??
|
||||
(await getViewportVolumeHistogram(viewport, volume, options));
|
||||
const { voi: displaySetVOI, colormap: displaySetColormap } =
|
||||
viewportInfo.displaySetOptions[volumeIndex];
|
||||
let colormap;
|
||||
if (displaySetColormap) {
|
||||
colormap =
|
||||
csUtils.colormap.getColormap(displaySetColormap.name) ??
|
||||
vtkColorMaps.getPresetByName(displaySetColormap.name);
|
||||
}
|
||||
|
||||
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
|
||||
|
||||
return {
|
||||
viewportId,
|
||||
modality,
|
||||
volumeId,
|
||||
volumeIndex,
|
||||
voi,
|
||||
histogram,
|
||||
colormap,
|
||||
step: scaling?.PT ? 0.05 : 1,
|
||||
opacity,
|
||||
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const data = windowLevels.filter(Boolean);
|
||||
return data;
|
||||
},
|
||||
|
||||
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
|
||||
);
|
||||
|
||||
const updateViewportHistograms = useCallback(() => {
|
||||
getWindowLevelsData(viewportId).then(windowLevels => {
|
||||
setWindowLevels(windowLevels);
|
||||
});
|
||||
}, [viewportId, getWindowLevelsData]);
|
||||
|
||||
const handleCornerstoneVOIModified = useCallback(
|
||||
e => {
|
||||
const { detail } = e;
|
||||
const { volumeId, range } = detail;
|
||||
const oldWindowLevel = windowLevels.find(wl => wl.volumeId === volumeId);
|
||||
|
||||
if (!oldWindowLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldVOI = oldWindowLevel.voi;
|
||||
const windowWidth = range.upper - range.lower;
|
||||
const windowCenter = range.lower + windowWidth / 2;
|
||||
|
||||
if (windowWidth === oldVOI.windowWidth && windowCenter === oldVOI.windowCenter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newWindowLevel = {
|
||||
...oldWindowLevel,
|
||||
voi: {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
},
|
||||
};
|
||||
|
||||
setWindowLevels(
|
||||
windowLevels.map(windowLevel =>
|
||||
windowLevel === oldWindowLevel ? newWindowLevel : windowLevel
|
||||
)
|
||||
);
|
||||
},
|
||||
[windowLevels]
|
||||
);
|
||||
|
||||
const debouncedHandleCornerstoneVOIModified = useCallback(
|
||||
debounce(handleCornerstoneVOIModified, 100),
|
||||
[handleCornerstoneVOIModified]
|
||||
);
|
||||
|
||||
const handleVOIChange = useCallback(
|
||||
(volumeId, voi) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const newRange = {
|
||||
lower: voi.windowCenter - voi.windowWidth / 2,
|
||||
upper: voi.windowCenter + voi.windowWidth / 2,
|
||||
};
|
||||
|
||||
viewport.setProperties({ voiRange: newRange }, volumeId);
|
||||
viewport.render();
|
||||
},
|
||||
[cornerstoneViewportService, viewportId]
|
||||
);
|
||||
|
||||
const handleOpacityChange = useCallback(
|
||||
(viewportId, _volumeIndex, volumeId, opacity) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
|
||||
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
||||
|
||||
viewports.forEach(vp => {
|
||||
vp.setProperties({ colormap: { opacity } }, volumeId);
|
||||
vp.render();
|
||||
});
|
||||
},
|
||||
[getViewportsWithVolumeIds, cornerstoneViewportService]
|
||||
);
|
||||
|
||||
// Listen to windowLevels changes and caches all the new ones
|
||||
useEffect(() => {
|
||||
const newVolumeHistograms = windowLevels
|
||||
.filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
|
||||
.reduce((volumeHistograms, windowLevel) => {
|
||||
// If the histogram exists, add it to the volumeHistograms object
|
||||
if (windowLevel?.histogram) {
|
||||
volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
|
||||
}
|
||||
return volumeHistograms;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(newVolumeHistograms).length) {
|
||||
setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
|
||||
}
|
||||
}, [windowLevels, cachedHistograms]);
|
||||
|
||||
// Updates the histogram when the viewport index prop has changed
|
||||
useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
|
||||
|
||||
// Listen to cornerstone events on "eventTarget" and at the document level
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
|
||||
|
||||
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||
updateViewportHistograms
|
||||
);
|
||||
|
||||
document.removeEventListener(
|
||||
Events.VOI_MODIFIED,
|
||||
debouncedHandleCornerstoneVOIModified,
|
||||
true
|
||||
);
|
||||
};
|
||||
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
|
||||
|
||||
// Updates the viewport when the context of the viewport has changed. This is
|
||||
// necessary when moving across different stages because the viewport index
|
||||
// may not change but the volumes loaded on it may change.
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
|
||||
({ viewportInfo }) => {
|
||||
if (viewportInfo.viewportId === viewportId) {
|
||||
updateViewportHistograms();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
|
||||
|
||||
return (
|
||||
<PanelSection title="Window Level">
|
||||
{windowLevels.map((windowLevel, i) => {
|
||||
if (!windowLevel.histogram) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WindowLevel
|
||||
key={windowLevel.volumeId}
|
||||
title={`${windowLevel.modality}`}
|
||||
histogram={windowLevel.histogram}
|
||||
voi={windowLevel.voi}
|
||||
step={windowLevel.step}
|
||||
showOpacitySlider={windowLevel.showOpacitySlider}
|
||||
colormap={windowLevel.colormap}
|
||||
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
|
||||
opacity={windowLevel.opacity}
|
||||
onOpacityChange={opacity =>
|
||||
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PanelSection>
|
||||
);
|
||||
};
|
||||
|
||||
ViewportWindowLevel.propTypes = {
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ViewportWindowLevel;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||
|
||||
const workerManager = getWebWorkerManager();
|
||||
|
||||
const WorkerOptions = {
|
||||
maxWorkerInstances: 1,
|
||||
autoTerminateOnIdle: {
|
||||
enabled: true,
|
||||
idleTimeThreshold: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
// Register the task
|
||||
const workerFn = () => {
|
||||
return new Worker(new URL('./histogramWorker.js', import.meta.url), {
|
||||
name: 'histogram-worker', // name used by the browser to name the worker
|
||||
});
|
||||
};
|
||||
|
||||
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
|
||||
|
||||
if (!volume?.loadStatus.loaded) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const volumeImageData = viewport.getImageData(volume.volumeId);
|
||||
|
||||
if (!volumeImageData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let scalarData = volume.scalarData;
|
||||
|
||||
let prevTimePoint;
|
||||
if (volume.numTimePoints > 1) {
|
||||
prevTimePoint = volume.timePointIndex;
|
||||
const middleTimePoint = Math.round(volume.numTimePoints / 2);
|
||||
volume.timePointIndex = middleTimePoint;
|
||||
scalarData = volume.getScalarData(middleTimePoint);
|
||||
}
|
||||
|
||||
const { dimensions, origin, direction, spacing } = volume;
|
||||
|
||||
const range = await workerManager.executeTask('histogram-worker', 'getRange', {
|
||||
dimensions,
|
||||
origin,
|
||||
direction,
|
||||
spacing,
|
||||
scalarData,
|
||||
});
|
||||
|
||||
// after we calculate the range let's reset the timePointIndex
|
||||
if (volume.numTimePoints > 1) {
|
||||
volume.timePointIndex = prevTimePoint;
|
||||
}
|
||||
const { minimum: min, maximum: max } = range;
|
||||
const calcHistOptions = {
|
||||
numBins: 256,
|
||||
min: Math.max(min, options?.min ?? min),
|
||||
max: Math.min(max, options?.max ?? max),
|
||||
};
|
||||
|
||||
const histogram = await workerManager.executeTask('histogram-worker', 'calcHistogram', {
|
||||
data: scalarData,
|
||||
options: calcHistOptions,
|
||||
});
|
||||
|
||||
return histogram;
|
||||
};
|
||||
|
||||
export { getViewportVolumeHistogram };
|
||||
@@ -0,0 +1,97 @@
|
||||
import { expose } from 'comlink';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
|
||||
/**
|
||||
* This object simulates a heavy task by implementing a sleep function and a recursive Fibonacci function.
|
||||
* It's used for testing or demonstrating purposes where a heavy or time-consuming task is needed.
|
||||
*/
|
||||
const obj = {
|
||||
getRange: ({ dimensions, origin, direction, spacing, scalarData }) => {
|
||||
const imageData = vtkImageData.newInstance();
|
||||
imageData.setDimensions(dimensions);
|
||||
imageData.setOrigin(origin);
|
||||
imageData.setDirection(direction);
|
||||
imageData.setSpacing(spacing);
|
||||
|
||||
const scalarArray = vtkDataArray.newInstance({
|
||||
name: 'Pixels',
|
||||
numberOfComponents: 1,
|
||||
values: scalarData,
|
||||
});
|
||||
|
||||
imageData.getPointData().setScalars(scalarArray);
|
||||
|
||||
imageData.modified();
|
||||
|
||||
const range = imageData.computeHistogram(imageData.getBounds());
|
||||
|
||||
return range;
|
||||
},
|
||||
calcHistogram: ({ data, options }) => {
|
||||
if (options === undefined) {
|
||||
options = {};
|
||||
}
|
||||
const histogram = {
|
||||
numBins: options.numBins || 256,
|
||||
range: { min: 0, max: 0 },
|
||||
bins: new Int32Array(1),
|
||||
maxBin: 0,
|
||||
maxBinValue: 0,
|
||||
};
|
||||
|
||||
let minToUse = options.min;
|
||||
let maxToUse = options.max;
|
||||
|
||||
if (minToUse === undefined || maxToUse === undefined) {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
let index = data.length;
|
||||
|
||||
while (index--) {
|
||||
const value = data[index];
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
|
||||
minToUse = min;
|
||||
maxToUse = max;
|
||||
}
|
||||
|
||||
histogram.range = { min: minToUse, max: maxToUse };
|
||||
|
||||
const bins = new Int32Array(histogram.numBins);
|
||||
const binScale = histogram.numBins / (maxToUse - minToUse);
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const value = data[index];
|
||||
if (value < minToUse) {
|
||||
continue;
|
||||
}
|
||||
if (value > maxToUse) {
|
||||
continue;
|
||||
}
|
||||
const bin = Math.floor((value - minToUse) * binScale);
|
||||
bins[bin] += 1;
|
||||
}
|
||||
|
||||
histogram.bins = bins;
|
||||
histogram.maxBin = 0;
|
||||
histogram.maxBinValue = 0;
|
||||
|
||||
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||
if (histogram.bins[bin] > histogram.maxBinValue) {
|
||||
histogram.maxBin = bin;
|
||||
histogram.maxBinValue = histogram.bins[bin];
|
||||
}
|
||||
}
|
||||
|
||||
return histogram;
|
||||
},
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ViewportWindowLevel';
|
||||
@@ -8,10 +8,10 @@ export function setViewportColorbar(
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
serviceManager,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
colorbarOptions
|
||||
) {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
@@ -59,10 +59,10 @@ export function Colorbar({
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
colorbarProperties,
|
||||
}: ColorbarProps): ReactElement {
|
||||
const { colorbarService } = serviceManager.services;
|
||||
}: withAppTypes<ColorbarProps>): ReactElement {
|
||||
const { colorbarService } = servicesManager.services;
|
||||
const {
|
||||
width: colorbarWidth,
|
||||
colorbarTickPosition,
|
||||
@@ -73,7 +73,7 @@ export function Colorbar({
|
||||
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, {
|
||||
viewportId,
|
||||
colormaps,
|
||||
ticks: {
|
||||
@@ -102,6 +102,7 @@ export function Colorbar({
|
||||
|
||||
return (
|
||||
<div className="all-in-one-menu-item flex w-full justify-center">
|
||||
<div className="mr-2 w-[28px]"></div>
|
||||
<SwitchButton
|
||||
label="Display Color bar"
|
||||
checked={showColorbar}
|
||||
|
||||
@@ -8,9 +8,9 @@ export function Colormap({
|
||||
viewportId,
|
||||
displaySets,
|
||||
commandsManager,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
}: ColormapProps): ReactElement {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
|
||||
|
||||
@@ -68,6 +68,11 @@ export function Colormap({
|
||||
key: index,
|
||||
style: {
|
||||
minWidth: `calc(100% / ${displaySets.length})`,
|
||||
fontSize: '0.8rem',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
}, [displaySets]);
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { ReactElement, useState, useEffect, useCallback } from 'react';
|
||||
import { VolumeLightingProps } from '../../types/ViewportPresets';
|
||||
|
||||
export function VolumeLighting({
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
viewportId,
|
||||
}: VolumeLightingProps): ReactElement {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [ambient, setAmbient] = useState(null);
|
||||
const [diffuse, setDiffuse] = useState(null);
|
||||
const [specular, setSpecular] = useState(null);
|
||||
@@ -60,7 +60,11 @@ export function VolumeLighting({
|
||||
min={0}
|
||||
type="range"
|
||||
step={0.1}
|
||||
style={{ background: calculateBackground(ambient) }}
|
||||
style={{
|
||||
background: calculateBackground(ambient),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -84,7 +88,11 @@ export function VolumeLighting({
|
||||
min={0}
|
||||
type="range"
|
||||
step={0.1}
|
||||
style={{ background: calculateBackground(diffuse) }}
|
||||
style={{
|
||||
background: calculateBackground(diffuse),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -109,7 +117,11 @@ export function VolumeLighting({
|
||||
min={0}
|
||||
type="range"
|
||||
step={0.1}
|
||||
style={{ background: calculateBackground(specular) }}
|
||||
style={{
|
||||
background: calculateBackground(specular),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+7
-7
@@ -9,37 +9,37 @@ export function VolumeRenderingOptions({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
volumeRenderingQualityRange,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
}: VolumeRenderingOptionsProps): ReactElement {
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
<VolumeRenderingQuality
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
/>
|
||||
|
||||
<VolumeShift
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
<div className="all-in-one-menu-item flex w-full justify-start">
|
||||
<div className="all-in-one-menu-item mt-2 flex !h-[20px] w-full justify-start">
|
||||
<div className="text-aqua-pale text-[13px]">LIGHTING</div>
|
||||
</div>
|
||||
<AllInOneMenu.DividerItem />
|
||||
<div className="bg-primary-dark mt-1 mb-1 h-[2px] w-full"></div>
|
||||
<div className="all-in-one-menu-item flex w-full justify-center">
|
||||
<VolumeShade
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
viewportId={viewportId}
|
||||
/>
|
||||
</div>
|
||||
<VolumeLighting
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</AllInOneMenu.ItemPanel>
|
||||
);
|
||||
|
||||
+3
-3
@@ -5,11 +5,11 @@ import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
|
||||
|
||||
export function VolumeRenderingPresets({
|
||||
viewportId,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
volumeRenderingPresets,
|
||||
}: VolumeRenderingPresetsProps): ReactElement {
|
||||
const { uiModalService } = serviceManager.services;
|
||||
const { uiModalService } = servicesManager.services;
|
||||
|
||||
const onClickPresets = () => {
|
||||
uiModalService.show({
|
||||
@@ -23,7 +23,7 @@ export function VolumeRenderingPresets({
|
||||
commandsManager,
|
||||
},
|
||||
containerDimensions: 'h-[543px] w-[460px]',
|
||||
contentDimensions: 'h-[493px] w-[460px] pl-[12px]',
|
||||
contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+7
-3
@@ -4,10 +4,10 @@ import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
|
||||
export function VolumeRenderingQuality({
|
||||
volumeRenderingQualityRange,
|
||||
commandsManager,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
viewportId,
|
||||
}: VolumeRenderingQualityProps): ReactElement {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const { min, max, step } = volumeRenderingQualityRange;
|
||||
const [quality, setQuality] = useState(null);
|
||||
|
||||
@@ -60,7 +60,11 @@ export function VolumeRenderingQuality({
|
||||
type="range"
|
||||
step={step}
|
||||
onChange={e => onChange(parseInt(e.target.value, 10))}
|
||||
style={{ background: calculateBackground((quality - min) / (max - min)) }}
|
||||
style={{
|
||||
background: calculateBackground((quality - min) / (max - min)),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { VolumeShadeProps } from '../../types/ViewportPresets';
|
||||
export function VolumeShade({
|
||||
commandsManager,
|
||||
viewportId,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
}: VolumeShadeProps): ReactElement {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [shade, setShade] = useState(true);
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { VolumeShiftProps } from '../../types/ViewportPresets';
|
||||
export function VolumeShift({
|
||||
viewportId,
|
||||
commandsManager,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
}: VolumeShiftProps): ReactElement {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [minShift, setMinShift] = useState<number | null>(null);
|
||||
const [maxShift, setMaxShift] = useState<number | null>(null);
|
||||
const [shift, setShift] = useState<number | null>(
|
||||
@@ -82,6 +82,8 @@ export function VolumeShift({
|
||||
step={step}
|
||||
style={{
|
||||
background: calculateBackground((shift - minShift) / (maxShift - minShift)),
|
||||
'--thumb-inner-color': '#5acce6',
|
||||
'--thumb-outer-color': '#090c29',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type WindowLevelProps = {
|
||||
viewportId: string;
|
||||
presets: Record<string, Array<WindowLevelPreset>>;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
commandsManager: CommandsManager;
|
||||
};
|
||||
|
||||
@@ -23,25 +23,34 @@ export function WindowLevel({
|
||||
commandName: 'setViewportWindowLevel',
|
||||
commandOptions: {
|
||||
...props,
|
||||
viewportId,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
[commandsManager, viewportId]
|
||||
);
|
||||
|
||||
return (
|
||||
<AllInOneMenu.ItemPanel>
|
||||
<AllInOneMenu.HeaderItem>
|
||||
{t('Modality Presets', { modality: Object.keys(presets)[0] })}
|
||||
</AllInOneMenu.HeaderItem>
|
||||
{Object.values(presets)[0].map((preset, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={index}
|
||||
label={preset.description}
|
||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
||||
onClick={() => onSetWindowLevel({ ...preset, viewportId })}
|
||||
></AllInOneMenu.Item>
|
||||
{presets.map((modalityPresets, modalityIndex) => (
|
||||
<React.Fragment key={modalityIndex}>
|
||||
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
|
||||
<React.Fragment key={modality}>
|
||||
<AllInOneMenu.HeaderItem>
|
||||
{t('Modality Presets', { modality })}
|
||||
</AllInOneMenu.HeaderItem>
|
||||
{presetsArray.map((preset, index) => (
|
||||
<AllInOneMenu.Item
|
||||
key={`${modality}-${index}`}
|
||||
label={preset.description}
|
||||
secondaryLabel={`${preset.window} / ${preset.level}`}
|
||||
onClick={() => onSetWindowLevel(preset)}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</AllInOneMenu.ItemPanel>
|
||||
);
|
||||
|
||||
+15
-25
@@ -2,7 +2,6 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import { AllInOneMenu, useViewportGrid } from '@ohif/ui';
|
||||
import { CommandsManager, ServicesManager } from '@ohif/core';
|
||||
import { Colormap } from './Colormap';
|
||||
import { Colorbar } from './Colorbar';
|
||||
import { setViewportColorbar } from './Colorbar';
|
||||
@@ -19,11 +18,9 @@ import { utilities } from '@cornerstonejs/core';
|
||||
export type WindowLevelActionMenuProps = {
|
||||
viewportId: string;
|
||||
element: HTMLElement;
|
||||
presets: Record<string, Array<WindowLevelPreset>>;
|
||||
presets: Array<Record<string, Array<WindowLevelPreset>>>;
|
||||
verticalDirection: AllInOneMenu.VerticalDirection;
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection;
|
||||
commandsManager: CommandsManager;
|
||||
serviceManager: ServicesManager;
|
||||
colorbarProperties: ColorbarProperties;
|
||||
displaySets: Array<any>;
|
||||
volumeRenderingPresets: Array<ViewportPreset>;
|
||||
@@ -37,12 +34,12 @@ export function WindowLevelActionMenu({
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
commandsManager,
|
||||
serviceManager,
|
||||
servicesManager,
|
||||
colorbarProperties,
|
||||
displaySets,
|
||||
volumeRenderingPresets,
|
||||
volumeRenderingQualityRange,
|
||||
}: WindowLevelActionMenuProps): ReactElement {
|
||||
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
|
||||
const {
|
||||
colormaps,
|
||||
colorbarContainerPosition,
|
||||
@@ -50,8 +47,9 @@ export function WindowLevelActionMenu({
|
||||
colorbarTickPosition,
|
||||
width: colorbarWidth,
|
||||
} = colorbarProperties;
|
||||
const { colorbarService, cornerstoneViewportService } = serviceManager.services;
|
||||
const { colorbarService, cornerstoneViewportService } = servicesManager.services;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const backgroundColor = viewportInfo.getViewportOptions().background;
|
||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
|
||||
@@ -67,7 +65,7 @@ export function WindowLevelActionMenu({
|
||||
const [is3DVolume, setIs3DVolume] = useState(false);
|
||||
|
||||
const onSetColorbar = useCallback(() => {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
|
||||
setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, {
|
||||
colormaps,
|
||||
ticks: {
|
||||
position: colorbarTickPosition,
|
||||
@@ -93,13 +91,7 @@ export function WindowLevelActionMenu({
|
||||
colorbarService.removeColorbar(viewportId);
|
||||
onSetColorbar();
|
||||
}, 0);
|
||||
}, [viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (colorbarService.hasColorbar(viewportId)) {
|
||||
colorbarService.removeColorbar(viewportId);
|
||||
}
|
||||
}, [displaySets]);
|
||||
}, [viewportId, displaySets, viewport]);
|
||||
|
||||
useEffect(() => {
|
||||
setMenuKey(menuKey + 1);
|
||||
@@ -128,10 +120,8 @@ export function WindowLevelActionMenu({
|
||||
iconClassName={classNames(
|
||||
// Visible on hover and for the active viewport
|
||||
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
|
||||
'flex shrink-0 cursor-pointer rounded active:text-white',
|
||||
isLight
|
||||
? 'text-aqua-pale hover:bg-secondary-dark'
|
||||
: 'text-primary-light hover:bg-secondary-light/60'
|
||||
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
|
||||
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
|
||||
)}
|
||||
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
|
||||
onVisibilityChange={() => {
|
||||
@@ -145,7 +135,7 @@ export function WindowLevelActionMenu({
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
colorbarProperties={colorbarProperties}
|
||||
/>
|
||||
)}
|
||||
@@ -161,15 +151,15 @@ export function WindowLevelActionMenu({
|
||||
viewportId={viewportId}
|
||||
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
{presets && !is3DVolume && (
|
||||
{presets && presets.length > 0 && !is3DVolume && (
|
||||
<AllInOneMenu.SubMenu
|
||||
key="windowLevelPresets"
|
||||
itemLabel={t('Modality Window Presets', { modality: Object.keys(presets)[0] })}
|
||||
itemLabel={t('Modality Window Presets')}
|
||||
itemIcon="viewport-window-level"
|
||||
>
|
||||
<WindowLevel
|
||||
@@ -182,7 +172,7 @@ export function WindowLevelActionMenu({
|
||||
|
||||
{volumeRenderingPresets && is3DVolume && (
|
||||
<VolumeRenderingPresets
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
@@ -195,7 +185,7 @@ export function WindowLevelActionMenu({
|
||||
viewportId={viewportId}
|
||||
commandsManager={commandsManager}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
serviceManager={serviceManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</AllInOneMenu.SubMenu>
|
||||
)}
|
||||
|
||||
+5
-7
@@ -9,7 +9,7 @@ export function getWindowLevelActionMenu({
|
||||
commandsManager,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
}): ReactNode {
|
||||
}: withAppTypes): ReactNode {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
|
||||
@@ -24,21 +24,19 @@ export function getWindowLevelActionMenu({
|
||||
return { [displaySet.Modality]: presets[displaySet.Modality] };
|
||||
});
|
||||
|
||||
const hasMenu = displaySetPresets.length > 0;
|
||||
|
||||
return hasMenu ? (
|
||||
return (
|
||||
<WindowLevelActionMenu
|
||||
viewportId={viewportId}
|
||||
element={element}
|
||||
presets={displaySetPresets[0]}
|
||||
presets={displaySetPresets}
|
||||
verticalDirection={verticalDirection}
|
||||
horizontalDirection={horizontalDirection}
|
||||
commandsManager={commandsManager}
|
||||
serviceManager={servicesManager}
|
||||
servicesManager={servicesManager}
|
||||
colorbarProperties={colorbarProperties}
|
||||
displaySets={displaySets}
|
||||
volumeRenderingPresets={volumeRenderingPresets}
|
||||
volumeRenderingQualityRange={volumeRenderingQualityRange}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
@@ -64,6 +64,136 @@ function getCustomizationModule() {
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cornerstone.measurements',
|
||||
Angle: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
CobbAngle: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
ArrowAnnotate: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
RectangleROi: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
CircleROI: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
EllipticalROI: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
Bidirectional: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
Length: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
LivewireContour: {
|
||||
displayText: [],
|
||||
report: [],
|
||||
},
|
||||
SplineROI: {
|
||||
displayText: [
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'areaUnit',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
/**
|
||||
{
|
||||
displayName: 'Modality',
|
||||
value: 'Modality',
|
||||
type: 'value',
|
||||
},
|
||||
*/
|
||||
],
|
||||
report: [
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'areaUnit',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
PlanarFreehandROI: {
|
||||
displayText: [
|
||||
{
|
||||
displayName: 'Mean',
|
||||
value: 'mean',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Max',
|
||||
value: 'max',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
value: 'modalityUnit',
|
||||
for: ['mean', 'max' /** 'stdDev **/],
|
||||
type: 'unit',
|
||||
},
|
||||
{
|
||||
value: 'areaUnit',
|
||||
for: ['area'],
|
||||
type: 'unit',
|
||||
},
|
||||
/**
|
||||
{
|
||||
displayName: 'Std Dev',
|
||||
value: 'stdDev',
|
||||
type: 'value',
|
||||
},
|
||||
*/
|
||||
],
|
||||
report: [
|
||||
{
|
||||
displayName: 'Mean',
|
||||
value: 'mean',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Max',
|
||||
value: 'max',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Area',
|
||||
value: 'area',
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
value: 'unit',
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -6,9 +6,10 @@ const getToggledClassName = (isToggled: boolean) => {
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
||||
};
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
|
||||
const {
|
||||
toolGroupService,
|
||||
toolbarService,
|
||||
syncGroupService,
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
@@ -21,16 +22,16 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
// enabled or not
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool',
|
||||
evaluate: ({ viewportId, button, disabledText }) => {
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolName = getToolNameForButton(button);
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
@@ -38,13 +39,15 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
}
|
||||
|
||||
const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName;
|
||||
const isPrimaryActive = toolNames
|
||||
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
|
||||
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black bg-primary-light'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
|
||||
? '!text-black bg-primary-light rounded'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light rounded',
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
@@ -71,7 +74,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
// check if the active toolName is part of the items then we need
|
||||
// to move it to the primary button
|
||||
const activeToolIndex = items.findIndex(item => {
|
||||
const toolName = getToolNameForButton(item);
|
||||
const toolName = toolbarService.getToolNameForButton(item);
|
||||
return toolName === activeToolName;
|
||||
});
|
||||
|
||||
@@ -108,32 +111,29 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled',
|
||||
evaluate: ({ viewportId, button, disabledText }) =>
|
||||
_evaluateToggle({
|
||||
viewportId,
|
||||
button,
|
||||
toolbarService,
|
||||
disabledText,
|
||||
offModes: [Enums.ToolModes.Disabled],
|
||||
toolGroupService,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstoneTool.toggle',
|
||||
evaluate: ({ viewportId, button, disabledText }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
|
||||
toolGroup.getToolOptions(toolName).mode
|
||||
);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
},
|
||||
evaluate: ({ viewportId, button, disabledText }) =>
|
||||
_evaluateToggle({
|
||||
viewportId,
|
||||
button,
|
||||
toolbarService,
|
||||
disabledText,
|
||||
offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive],
|
||||
toolGroupService,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.synchronizer',
|
||||
@@ -146,7 +146,11 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
};
|
||||
}
|
||||
|
||||
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
|
||||
const isArray = Array.isArray(button.commands);
|
||||
|
||||
const synchronizerType = isArray
|
||||
? button.commands?.[0].commandOptions.type
|
||||
: button.commands?.commandOptions.type;
|
||||
|
||||
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
|
||||
|
||||
@@ -192,7 +196,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||
const isUS = displaySets.some(displaySet => displaySet.Modality === 'US');
|
||||
const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
|
||||
if (!isUS) {
|
||||
return {
|
||||
disabled: true,
|
||||
@@ -268,21 +272,32 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
];
|
||||
}
|
||||
|
||||
function getToolNameForButton(button) {
|
||||
const { props } = button;
|
||||
function _evaluateToggle({
|
||||
viewportId,
|
||||
toolbarService,
|
||||
button,
|
||||
disabledText,
|
||||
offModes,
|
||||
toolGroupService,
|
||||
}) {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
const commands = props?.commands || button.commands;
|
||||
const commandsArray = Array.isArray(commands) ? commands : [commands];
|
||||
const firstCommand = commandsArray[0];
|
||||
if (typeof firstCommand === 'string') {
|
||||
// likely not a cornerstone tool
|
||||
return null;
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName)) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
if ('commandOptions' in firstCommand) {
|
||||
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
|
||||
}
|
||||
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
// use id as a fallback for toolName
|
||||
return props?.id ?? button.id;
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
};
|
||||
}
|
||||
@@ -77,8 +77,11 @@ export const fourUp = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -77,8 +77,11 @@ export const main3D = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,17 +14,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
|
||||
numberOfPriorsReferenced: 0,
|
||||
protocolMatchingRules: [],
|
||||
imageLoadStrategy: 'nth',
|
||||
callbacks: {
|
||||
// Switches out of MPR mode when the layout change button is used
|
||||
onLayoutChange: [
|
||||
{
|
||||
commandName: 'toggleHangingProtocol',
|
||||
commandOptions: { protocolId: 'mpr' },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
// Turns off crosshairs when switching out of MPR mode
|
||||
},
|
||||
callbacks: {},
|
||||
displaySetSelectors: {
|
||||
activeDisplaySet: {
|
||||
seriesMatchingRules: [
|
||||
|
||||
@@ -84,7 +84,11 @@ export const mprAnd3DVolumeViewport = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -51,8 +51,11 @@ export const only3D = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -77,8 +77,11 @@ export const primary3D = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
// ToDo: choose appropriate preset
|
||||
displayPreset: 'CT-Bone',
|
||||
displayPreset: {
|
||||
CT: 'CT-Bone',
|
||||
MR: 'MR-Default',
|
||||
default: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
imageLoadPoolManager,
|
||||
imageRetrievalPoolManager,
|
||||
} from '@cornerstonejs/core';
|
||||
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import init from './init';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
@@ -34,6 +35,10 @@ import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
import { showLabelAnnotationPopup } from './utils/callInputDialog';
|
||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
|
||||
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
|
||||
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
||||
@@ -56,7 +61,26 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
onModeExit: (): void => {
|
||||
onModeEnter: ({ servicesManager }: withAppTypes): void => {
|
||||
const { cornerstoneViewportService, toolbarService, segmentationService } =
|
||||
servicesManager.services;
|
||||
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
|
||||
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
|
||||
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
|
||||
]);
|
||||
},
|
||||
|
||||
onModeExit: ({ servicesManager }: withAppTypes): void => {
|
||||
const { cineService } = servicesManager.services;
|
||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||
// on the mode exits
|
||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||
@@ -64,6 +88,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
cineService.setIsCineEnabled(false);
|
||||
|
||||
enabledElementReset();
|
||||
},
|
||||
|
||||
@@ -90,13 +116,23 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
|
||||
getToolbarModule,
|
||||
getPanelModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'activeViewportWindowLevel',
|
||||
component: () => {
|
||||
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
// const onNewImageHandler = jumpData => {
|
||||
// commandsManager.runCommand('jumpToImage', jumpData);
|
||||
// };
|
||||
const { toolbarService } = (servicesManager as ServicesManager).services;
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
return (
|
||||
<OHIFCornerstoneViewport
|
||||
@@ -127,7 +163,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
getEnabledElement,
|
||||
dicomLoaderService,
|
||||
showLabelAnnotationPopup
|
||||
showLabelAnnotationPopup,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -143,6 +179,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
Enums: cs3DToolsEnums,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'volumeLoader',
|
||||
exports: {
|
||||
getDynamicVolumeInfo,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
utilities as csUtilities,
|
||||
Enums as csEnums,
|
||||
} from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||
|
||||
import initWADOImageLoader from './initWADOImageLoader';
|
||||
import initCornerstoneTools from './initCornerstoneTools';
|
||||
@@ -27,12 +30,10 @@ import nthLoader from './utils/nthLoader';
|
||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||
import initContextMenu from './initContextMenu';
|
||||
import initDoubleClick from './initDoubleClick';
|
||||
import { CornerstoneServices } from './types';
|
||||
import initViewTiming from './utils/initViewTiming';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { colormaps } from './utils/colormaps';
|
||||
|
||||
const { registerColormap } = utilities.colormap;
|
||||
const { registerColormap } = csUtilities.colormap;
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
window.cornerstone = cornerstone;
|
||||
@@ -93,28 +94,11 @@ export default async function init({
|
||||
customizationService,
|
||||
uiModalService,
|
||||
uiNotificationService,
|
||||
cineService,
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
toolbarService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
segmentationService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
|
||||
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(segmentationService, [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
]);
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(eventTarget, [
|
||||
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
|
||||
]);
|
||||
} = servicesManager.services;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
window.extensionManager = extensionManager;
|
||||
@@ -157,14 +141,19 @@ export default async function init({
|
||||
});
|
||||
|
||||
const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
const contourRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Contour;
|
||||
|
||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
|
||||
fillAlpha: 0.3,
|
||||
fillAlpha: 0.5,
|
||||
fillAlphaInactive: 0.2,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.65,
|
||||
});
|
||||
|
||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(contourRepresentation, {
|
||||
renderFill: false,
|
||||
});
|
||||
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
volumeLoader.registerVolumeLoader(
|
||||
@@ -172,6 +161,11 @@ export default async function init({
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
|
||||
volumeLoader.registerVolumeLoader(
|
||||
'cornerstoneStreamingDynamicImageVolume',
|
||||
cornerstoneStreamingDynamicImageVolumeLoader
|
||||
);
|
||||
|
||||
hangingProtocolService.registerImageLoadStrategy('interleaveCenter', interleaveCenterLoader);
|
||||
hangingProtocolService.registerImageLoadStrategy('interleaveTopToBottom', interleaveTopToBottom);
|
||||
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
|
||||
@@ -195,7 +189,7 @@ export default async function init({
|
||||
/* Measurement Service */
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
|
||||
|
||||
initCineService(cineService);
|
||||
initCineService(servicesManager);
|
||||
|
||||
// When a custom image load is performed, update the relevant viewports
|
||||
hangingProtocolService.subscribe(
|
||||
@@ -285,8 +279,20 @@ export default async function init({
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
|
||||
colormaps.forEach(registerColormap);
|
||||
|
||||
// Event listener
|
||||
eventTarget.addEventListenerDebounced(
|
||||
EVENTS.ERROR_EVENT,
|
||||
({ detail }) => {
|
||||
uiNotificationService.show({
|
||||
title: detail.type,
|
||||
message: detail.message,
|
||||
type: 'error',
|
||||
});
|
||||
},
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
function CPUModal() {
|
||||
|
||||
@@ -1,15 +1,73 @@
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
function initCineService(cineService) {
|
||||
function _getVolumesFromViewport(viewport) {
|
||||
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
|
||||
}
|
||||
|
||||
function _getVolumeFromViewport(viewport) {
|
||||
const volumes = _getVolumesFromViewport(viewport);
|
||||
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
|
||||
|
||||
return dynamicVolume ?? volumes[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all viewports that needs to be synchronized with the source
|
||||
* viewport passed as parameter when cine is updated.
|
||||
* @param servicesManager ServiceManager
|
||||
* @param srcViewportIndex Source viewport index
|
||||
* @returns array with viewport information.
|
||||
*/
|
||||
function _getSyncedViewports(servicesManager: AppTypes.ServicesManager, srcViewportId) {
|
||||
const { viewportGridService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const { viewports: viewportsStates } = viewportGridService.getState();
|
||||
const srcViewportState = viewportsStates.get(srcViewportId);
|
||||
|
||||
if (srcViewportState?.viewportOptions?.viewportType !== 'volume') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const srcViewport = cornerstoneViewportService.getCornerstoneViewport(srcViewportId);
|
||||
|
||||
const srcVolume = srcViewport ? _getVolumeFromViewport(srcViewport) : null;
|
||||
|
||||
if (!srcVolume?.isDynamicVolume()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { volumeId: srcVolumeId } = srcVolume;
|
||||
|
||||
return Array.from(viewportsStates.values())
|
||||
.filter(({ viewportId }) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
return viewportId !== srcViewportId && viewport?.hasVolumeId(srcVolumeId);
|
||||
})
|
||||
.map(({ viewportId }) => ({ viewportId }));
|
||||
}
|
||||
|
||||
function initCineService(servicesManager: AppTypes.ServicesManager) {
|
||||
const { cineService } = servicesManager.services;
|
||||
|
||||
const getSyncedViewports = viewportId => {
|
||||
return _getSyncedViewports(servicesManager, viewportId);
|
||||
};
|
||||
|
||||
const playClip = (element, playClipOptions) => {
|
||||
return utilities.cine.playClip(element, playClipOptions);
|
||||
};
|
||||
|
||||
const stopClip = element => {
|
||||
return utilities.cine.stopClip(element);
|
||||
const stopClip = (element, stopClipOptions) => {
|
||||
return utilities.cine.stopClip(element, stopClipOptions);
|
||||
};
|
||||
|
||||
cineService.setServiceImplementation({ playClip, stopClip });
|
||||
cineService.setServiceImplementation({
|
||||
getSyncedViewports,
|
||||
playClip,
|
||||
stopClip,
|
||||
});
|
||||
}
|
||||
|
||||
export default initCineService;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MIPJumpToClickTool,
|
||||
LengthTool,
|
||||
RectangleROITool,
|
||||
RectangleROIThresholdTool,
|
||||
EllipticalROITool,
|
||||
CircleROITool,
|
||||
BidirectionalTool,
|
||||
@@ -19,19 +20,22 @@ import {
|
||||
MagnifyTool,
|
||||
CrosshairsTool,
|
||||
SegmentationDisplayTool,
|
||||
RectangleScissorsTool,
|
||||
SphereScissorsTool,
|
||||
CircleScissorsTool,
|
||||
BrushTool,
|
||||
PaintFillTool,
|
||||
init,
|
||||
addTool,
|
||||
annotation,
|
||||
ReferenceLinesTool,
|
||||
TrackballRotateTool,
|
||||
CircleScissorsTool,
|
||||
RectangleScissorsTool,
|
||||
SphereScissorsTool,
|
||||
AdvancedMagnifyTool,
|
||||
UltrasoundDirectionalTool,
|
||||
PlanarFreehandROITool,
|
||||
SplineROITool,
|
||||
LivewireContourTool,
|
||||
OrientationMarkerTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import CalibrationLineTool from './tools/CalibrationLineTool';
|
||||
@@ -40,6 +44,7 @@ import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
export default function initCornerstoneTools(configuration = {}) {
|
||||
CrosshairsTool.isAnnotation = false;
|
||||
ReferenceLinesTool.isAnnotation = false;
|
||||
AdvancedMagnifyTool.isAnnotation = false;
|
||||
|
||||
init(configuration);
|
||||
addTool(PanTool);
|
||||
@@ -52,6 +57,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(MIPJumpToClickTool);
|
||||
addTool(LengthTool);
|
||||
addTool(RectangleROITool);
|
||||
addTool(RectangleROIThresholdTool);
|
||||
addTool(EllipticalROITool);
|
||||
addTool(CircleROITool);
|
||||
addTool(BidirectionalTool);
|
||||
@@ -62,18 +68,21 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(MagnifyTool);
|
||||
addTool(CrosshairsTool);
|
||||
addTool(SegmentationDisplayTool);
|
||||
addTool(RectangleScissorsTool);
|
||||
addTool(SphereScissorsTool);
|
||||
addTool(CircleScissorsTool);
|
||||
addTool(BrushTool);
|
||||
addTool(PaintFillTool);
|
||||
addTool(ReferenceLinesTool);
|
||||
addTool(CalibrationLineTool);
|
||||
addTool(TrackballRotateTool);
|
||||
addTool(CircleScissorsTool);
|
||||
addTool(RectangleScissorsTool);
|
||||
addTool(SphereScissorsTool);
|
||||
addTool(ImageOverlayViewerTool);
|
||||
addTool(AdvancedMagnifyTool);
|
||||
addTool(UltrasoundDirectionalTool);
|
||||
addTool(PlanarFreehandROITool);
|
||||
addTool(SplineROITool);
|
||||
addTool(LivewireContourTool);
|
||||
addTool(OrientationMarkerTool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
@@ -103,6 +112,7 @@ const toolNames = {
|
||||
DragProbe: DragProbeTool.toolName,
|
||||
Probe: ProbeTool.toolName,
|
||||
RectangleROI: RectangleROITool.toolName,
|
||||
RectangleROIThreshold: RectangleROIThresholdTool.toolName,
|
||||
EllipticalROI: EllipticalROITool.toolName,
|
||||
CircleROI: CircleROITool.toolName,
|
||||
Bidirectional: BidirectionalTool.toolName,
|
||||
@@ -111,6 +121,8 @@ const toolNames = {
|
||||
Magnify: MagnifyTool.toolName,
|
||||
Crosshairs: CrosshairsTool.toolName,
|
||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||
Brush: BrushTool.toolName,
|
||||
PaintFill: PaintFillTool.toolName,
|
||||
ReferenceLines: ReferenceLinesTool.toolName,
|
||||
CalibrationLine: CalibrationLineTool.toolName,
|
||||
TrackballRotateTool: TrackballRotateTool.toolName,
|
||||
@@ -123,6 +135,7 @@ const toolNames = {
|
||||
SplineROI: SplineROITool.toolName,
|
||||
LivewireContour: LivewireContourTool.toolName,
|
||||
PlanarFreehandROI: PlanarFreehandROITool.toolName,
|
||||
OrientationMarker: OrientationMarkerTool.toolName,
|
||||
};
|
||||
|
||||
export { toolNames };
|
||||
+73
-7
@@ -17,7 +17,8 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
const initMeasurementService = (
|
||||
measurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
cornerstoneViewportService,
|
||||
customizationService
|
||||
) => {
|
||||
/* Initialization */
|
||||
const {
|
||||
@@ -30,10 +31,15 @@ const initMeasurementService = (
|
||||
CobbAngle,
|
||||
RectangleROI,
|
||||
PlanarFreehandROI,
|
||||
SplineROI,
|
||||
LivewireContour,
|
||||
Probe,
|
||||
UltrasoundDirectional,
|
||||
} = measurementServiceMappingsFactory(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
cornerstoneViewportService,
|
||||
customizationService
|
||||
);
|
||||
const csTools3DVer1MeasurementSource = measurementService.createSource(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
@@ -49,6 +55,20 @@ const initMeasurementService = (
|
||||
Length.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Crosshairs',
|
||||
Length.matchingCriteria,
|
||||
() => {
|
||||
console.warn('Crosshairs mapping not implemented.');
|
||||
return {};
|
||||
},
|
||||
() => {
|
||||
console.warn('Crosshairs mapping not implemented.');
|
||||
return {};
|
||||
}
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Bidirectional',
|
||||
@@ -113,6 +133,14 @@ const initMeasurementService = (
|
||||
PlanarFreehandROI.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'SplineROI',
|
||||
SplineROI.matchingCriteria,
|
||||
SplineROI.toAnnotation,
|
||||
SplineROI.toMeasurement
|
||||
);
|
||||
|
||||
// On the UI side, the Calibration Line tool will work almost the same as the
|
||||
// Length tool
|
||||
measurementService.addMapping(
|
||||
@@ -123,16 +151,45 @@ const initMeasurementService = (
|
||||
Length.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'LivewireContour',
|
||||
LivewireContour.matchingCriteria,
|
||||
LivewireContour.toAnnotation,
|
||||
LivewireContour.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Probe',
|
||||
Probe.matchingCriteria,
|
||||
Probe.toAnnotation,
|
||||
Probe.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'UltrasoundDirectionalTool',
|
||||
UltrasoundDirectional.matchingCriteria,
|
||||
UltrasoundDirectional.toAnnotation,
|
||||
UltrasoundDirectional.toMeasurement
|
||||
);
|
||||
|
||||
return csTools3DVer1MeasurementSource;
|
||||
};
|
||||
|
||||
const connectToolsToMeasurementService = servicesManager => {
|
||||
const { measurementService, displaySetService, cornerstoneViewportService } =
|
||||
servicesManager.services;
|
||||
const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesManager) => {
|
||||
const {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const csTools3DVer1MeasurementSource = initMeasurementService(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
cornerstoneViewportService,
|
||||
customizationService
|
||||
);
|
||||
connectMeasurementServiceToTools(
|
||||
measurementService,
|
||||
@@ -357,6 +414,10 @@ const connectMeasurementServiceToTools = (
|
||||
imageId = dataSource.getImageIdsForInstance({ instance });
|
||||
}
|
||||
|
||||
/**
|
||||
* This annotation is used by the cornerstone viewport.
|
||||
* This is not the read-only annotation rendered by the SR viewport.
|
||||
*/
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
annotationManager.addAnnotation({
|
||||
annotationUID: measurement.uid,
|
||||
@@ -369,11 +430,16 @@ const connectMeasurementServiceToTools = (
|
||||
referencedImageId: imageId,
|
||||
},
|
||||
data: {
|
||||
/**
|
||||
* Don't remove this destructuring of data here.
|
||||
* This is used to pass annotation specific data forward e.g. contour
|
||||
*/
|
||||
...(data.annotation.data || {}),
|
||||
text: data.annotation.data.text,
|
||||
handles: { ...data.annotation.data.handles },
|
||||
cachedStats: { ...data.annotation.data.cachedStats },
|
||||
label: data.annotation.data.label,
|
||||
frameNumber: frameNumber,
|
||||
frameNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import { volumeLoader } from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { errorHandler, utils } from '@ohif/core';
|
||||
@@ -41,6 +44,11 @@ export default function initWADOImageLoader(
|
||||
|
||||
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
||||
|
||||
registerVolumeLoader(
|
||||
'cornerstoneStreamingDynamicImageVolume',
|
||||
cornerstoneStreamingDynamicImageVolumeLoader
|
||||
);
|
||||
|
||||
dicomImageLoader.configure({
|
||||
decodeConfig: {
|
||||
// !! IMPORTANT !!
|
||||
|
||||
@@ -75,10 +75,7 @@ export default class ColorbarService extends PubSubService {
|
||||
|
||||
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
|
||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
|
||||
if (!actorEntry) {
|
||||
return;
|
||||
}
|
||||
const { uid: volumeId } = actorEntry;
|
||||
const volumeId = actorEntry?.uid;
|
||||
const properties = viewport?.getProperties(volumeId);
|
||||
const colormap = properties?.colormap;
|
||||
// if there's an initial colormap set, and no colormap on the viewport, set it
|
||||
@@ -194,8 +191,10 @@ export default class ColorbarService extends PubSubService {
|
||||
private setViewportColormap(viewportId, displaySetInstanceUID, colormap, immediate = false) {
|
||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||
const viewport = renderingEngine.getViewport(viewportId);
|
||||
const actorEntries = viewport.getActors();
|
||||
|
||||
const actorEntries = viewport?.getActors();
|
||||
if (!viewport || !actorEntries || actorEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
const setViewportProperties = (viewport, uid) => {
|
||||
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
|
||||
const { actor: volumeActor, uid: volumeId } = actorEntry;
|
||||
|
||||
+51
-21
@@ -1,4 +1,4 @@
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
import { Types } from '@ohif/core';
|
||||
import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
@@ -17,9 +17,9 @@ class CornerstoneCacheService {
|
||||
|
||||
stackImageIds: Map<string, string[]> = new Map();
|
||||
volumeImageIds: Map<string, string[]> = new Map();
|
||||
readonly servicesManager: ServicesManager;
|
||||
readonly servicesManager: AppTypes.ServicesManager;
|
||||
|
||||
constructor(servicesManager: ServicesManager) {
|
||||
constructor(servicesManager: AppTypes.ServicesManager) {
|
||||
this.servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,11 @@ class CornerstoneCacheService {
|
||||
// as a reference volume, if so, we should hang a volume viewport
|
||||
// instead of a stack viewport
|
||||
if (this._shouldRenderSegmentation(displaySets)) {
|
||||
viewportType = 'volume';
|
||||
// if the viewport type is volume 3D, we should let it be as it is
|
||||
// Todo: in future here we should kick start the conversion of the
|
||||
// segmentation to closed surface
|
||||
viewportType =
|
||||
viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
|
||||
|
||||
// update viewportOptions to reflect the new viewport type
|
||||
viewportOptions.viewportType = viewportType;
|
||||
@@ -94,7 +98,21 @@ class CornerstoneCacheService {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (volume) {
|
||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||
if (volume.imageIds) {
|
||||
// also for each imageId in the volume, remove the imageId from the cache
|
||||
// since that will hold the old metadata as well
|
||||
|
||||
volume.imageIds.forEach(imageId => {
|
||||
if (cs3DCache.getImageLoadObject(imageId)) {
|
||||
cs3DCache.removeImageLoadObject(imageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// this shouldn't be via removeVolumeLoadObject, since that will
|
||||
// remove the texture as well, but here we really just need a remove
|
||||
// from registry so that we load it again
|
||||
cs3DCache._volumeCache.delete(volumeId);
|
||||
this.volumeImageIds.delete(volumeId);
|
||||
}
|
||||
|
||||
@@ -111,14 +129,25 @@ class CornerstoneCacheService {
|
||||
return newViewportData;
|
||||
}
|
||||
|
||||
private _getStackViewportData(
|
||||
private async _getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex,
|
||||
viewportType: Enums.ViewportType
|
||||
): StackViewportData {
|
||||
// For Stack Viewport we don't have fusion currently
|
||||
const displaySet = displaySets[0];
|
||||
): Promise<StackViewportData> {
|
||||
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet);
|
||||
const nonOverlayDisplaySets = displaySets.filter(ds => !ds.isOverlayDisplaySet);
|
||||
|
||||
// load overlays if they are not loaded
|
||||
for (const overlayDisplaySet of overlayDisplaySets) {
|
||||
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
|
||||
const { userAuthenticationService } = this.servicesManager.services;
|
||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||
await overlayDisplaySet.load({ headers });
|
||||
}
|
||||
}
|
||||
|
||||
const displaySet = nonOverlayDisplaySets[0];
|
||||
|
||||
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
|
||||
|
||||
@@ -127,23 +156,23 @@ class CornerstoneCacheService {
|
||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||
}
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
|
||||
// Ensuring the first non-overlay `displaySet` is always the primary one
|
||||
const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => {
|
||||
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds;
|
||||
|
||||
const StackViewportData: StackViewportData = {
|
||||
viewportType,
|
||||
data: {
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
isCompositeStack,
|
||||
imageIds: stackImageIds,
|
||||
},
|
||||
initialImageIndex,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
viewportType,
|
||||
data: StackViewportData,
|
||||
};
|
||||
|
||||
if (typeof initialImageIndex === 'number') {
|
||||
StackViewportData.data.initialImageIndex = initialImageIndex;
|
||||
}
|
||||
|
||||
return StackViewportData;
|
||||
}
|
||||
|
||||
private async _getVolumeViewportData(
|
||||
@@ -200,6 +229,7 @@ class CornerstoneCacheService {
|
||||
volume,
|
||||
volumeId,
|
||||
imageIds: volumeImageIds,
|
||||
isDynamicVolume: displaySet.isDynamicVolume,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +258,7 @@ class CornerstoneCacheService {
|
||||
|
||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||
viewportDisplaySetInstanceUIDs,
|
||||
instance.FrameOfReferenceUID
|
||||
instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Types as OhifTypes, ServicesManager, PubSubService } from '@ohif/core';
|
||||
import { Types as OhifTypes, PubSubService } from '@ohif/core';
|
||||
import {
|
||||
cache,
|
||||
Enums as csEnums,
|
||||
@@ -60,7 +60,7 @@ class SegmentationService extends PubSubService {
|
||||
};
|
||||
|
||||
segmentations: Record<string, Segmentation>;
|
||||
readonly servicesManager: ServicesManager;
|
||||
readonly servicesManager: AppTypes.ServicesManager;
|
||||
highlightIntervalId = null;
|
||||
readonly EVENTS = EVENTS;
|
||||
|
||||
@@ -1015,6 +1015,8 @@ class SegmentationService extends PubSubService {
|
||||
): Promise<void> => {
|
||||
const segmentation = this.getSegmentation(segmentationId);
|
||||
|
||||
toolGroupId = toolGroupId || this._getApplicableToolGroupId();
|
||||
|
||||
if (!segmentation) {
|
||||
throw new Error(`Segmentation with segmentationId ${segmentationId} not found.`);
|
||||
}
|
||||
@@ -1147,6 +1149,10 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
displaySet.isHydrated = isHydrated;
|
||||
displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||||
segmentation: this.getSegmentation(displaySetUID),
|
||||
});
|
||||
}
|
||||
|
||||
private _highlightLabelmap(
|
||||
@@ -1289,7 +1295,7 @@ class SegmentationService extends PubSubService {
|
||||
}
|
||||
|
||||
const { colorLUTIndex } = segmentation;
|
||||
this._removeSegmentationFromCornerstone(segmentationId);
|
||||
const { updatedToolGroupIds } = this._removeSegmentationFromCornerstone(segmentationId);
|
||||
|
||||
// Delete associated colormap
|
||||
// Todo: bring this back
|
||||
@@ -1309,7 +1315,9 @@ class SegmentationService extends PubSubService {
|
||||
if (remainingHydratedSegmentations.length) {
|
||||
const { id } = remainingHydratedSegmentations[0];
|
||||
|
||||
this._setActiveSegmentationForToolGroup(id, this._getApplicableToolGroupId(), false);
|
||||
updatedToolGroupIds.forEach(toolGroupId => {
|
||||
this._setActiveSegmentationForToolGroup(id, toolGroupId, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1966,6 +1974,7 @@ class SegmentationService extends PubSubService {
|
||||
const removeFromCache = true;
|
||||
const segmentationState = cstSegmentation.state;
|
||||
const sourceSegState = segmentationState.getSegmentation(segmentationId);
|
||||
const updatedToolGroupIds: Set<string> = new Set();
|
||||
|
||||
if (!sourceSegState) {
|
||||
return;
|
||||
@@ -1981,6 +1990,7 @@ class SegmentationService extends PubSubService {
|
||||
segmentationRepresentations.forEach(representation => {
|
||||
if (representation.segmentationId === segmentationId) {
|
||||
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
||||
updatedToolGroupIds.add(toolGroupId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1998,6 +2008,8 @@ class SegmentationService extends PubSubService {
|
||||
if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) {
|
||||
cache.removeVolumeLoadObject(segmentationId);
|
||||
}
|
||||
|
||||
return { updatedToolGroupIds: Array.from(updatedToolGroupIds) };
|
||||
}
|
||||
|
||||
private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
|
||||
import { getRenderingEngines } from '@cornerstonejs/core';
|
||||
import { getRenderingEngines, utilities } from '@cornerstonejs/core';
|
||||
|
||||
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
|
||||
import { pubSubServiceInterface, Types } from '@ohif/core';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||
@@ -40,7 +40,7 @@ export default class SyncGroupService {
|
||||
},
|
||||
};
|
||||
|
||||
servicesManager: ServicesManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||
EVENTS: { [key: string]: string };
|
||||
synchronizerCreators: Record<string, SyncCreator> = {
|
||||
@@ -55,8 +55,8 @@ export default class SyncGroupService {
|
||||
|
||||
synchronizersByType: { [key: string]: Synchronizer[] } = {};
|
||||
|
||||
constructor(serviceManager: ServicesManager) {
|
||||
this.servicesManager = serviceManager;
|
||||
constructor(servicesManager: AppTypes.ServicesManager) {
|
||||
this.servicesManager = servicesManager;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
//
|
||||
@@ -184,6 +184,11 @@ export default class SyncGroupService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only image slice synchronizer register spatial registration
|
||||
if (this.isImageSliceSyncronizer(synchronizer)) {
|
||||
this.unRegisterSpatialRegistration(synchronizer);
|
||||
}
|
||||
|
||||
synchronizer.remove({
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
@@ -198,4 +203,45 @@ export default class SyncGroupService {
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Clean up the spatial registration metadata created by synchronizer
|
||||
* This is needed to be able to re-sync images slices if needed
|
||||
* @param synchronizer
|
||||
*/
|
||||
unRegisterSpatialRegistration(synchronizer: Synchronizer) {
|
||||
const sourceViewports = synchronizer.getSourceViewports().map(vp => vp.viewportId);
|
||||
const targetViewports = synchronizer.getTargetViewports().map(vp => vp.viewportId);
|
||||
|
||||
// Create an array of pair of viewports to remove from spatialRegistrationMetadataProvider
|
||||
// All sourceViewports combined with all targetViewports
|
||||
const toUnregister = sourceViewports
|
||||
.map((sourceViewportId: string) => {
|
||||
return targetViewports.map(targetViewportId => [targetViewportId, sourceViewportId]);
|
||||
})
|
||||
.reduce((acc, c) => acc.concat(c), []);
|
||||
|
||||
toUnregister.forEach(viewportIdPair => {
|
||||
utilities.spatialRegistrationMetadataProvider.add(viewportIdPair, undefined);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Check if the synchronizer type is IMAGE_SLICE
|
||||
* Need to convert to lowercase here because the types are lowercase
|
||||
* e.g: synchronizerCreators
|
||||
* @param synchronizer
|
||||
*/
|
||||
isImageSliceSyncronizer(synchronizer: Synchronizer) {
|
||||
return this.getSynchronizerType(synchronizer).toLowerCase() === IMAGE_SLICE;
|
||||
}
|
||||
/**
|
||||
* Returns the syncronizer type
|
||||
* @param synchronizer
|
||||
*/
|
||||
getSynchronizerType(synchronizer: Synchronizer): string {
|
||||
const synchronizerTypes = Object.keys(this.synchronizersByType);
|
||||
const syncType = synchronizerTypes.find(syncType =>
|
||||
this.getSynchronizersOfType(syncType).includes(synchronizer)
|
||||
);
|
||||
return syncType;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
||||
import { eventTarget } from '@cornerstonejs/core';
|
||||
|
||||
import { Types as OhifTypes, pubSubServiceInterface } from '@ohif/core';
|
||||
import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnabledElement';
|
||||
@@ -6,6 +7,8 @@ import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnable
|
||||
const EVENTS = {
|
||||
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
|
||||
TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:toolactivated',
|
||||
PRIMARY_TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:primarytoolactivated',
|
||||
};
|
||||
|
||||
type Tool = {
|
||||
@@ -29,7 +32,10 @@ export default class ToolGroupService {
|
||||
},
|
||||
};
|
||||
|
||||
serviceManager: any;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
cornerstoneViewportService: any;
|
||||
viewportGridService: any;
|
||||
uiNotificationService: any;
|
||||
private toolGroupIds: Set<string> = new Set();
|
||||
/**
|
||||
* Service-specific
|
||||
@@ -37,19 +43,27 @@ export default class ToolGroupService {
|
||||
listeners: { [key: string]: Function[] };
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor(serviceManager) {
|
||||
const { cornerstoneViewportService, viewportGridService } = serviceManager.services;
|
||||
constructor(servicesManager: AppTypes.ServicesManager) {
|
||||
const { cornerstoneViewportService, viewportGridService, uiNotificationService } =
|
||||
servicesManager.services;
|
||||
this.cornerstoneViewportService = cornerstoneViewportService;
|
||||
this.viewportGridService = viewportGridService;
|
||||
this.uiNotificationService = uiNotificationService;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
onModeExit() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
eventTarget.addEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a tool group from the ToolGroupManager by tool group ID.
|
||||
* If no tool group ID is provided, it retrieves the tool group of the active viewport.
|
||||
@@ -105,12 +119,14 @@ export default class ToolGroupService {
|
||||
return toolGroup.getActivePrimaryMouseButtonTool();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
ToolGroupManager.destroy();
|
||||
this.toolGroupIds = new Set();
|
||||
|
||||
eventTarget.removeEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated);
|
||||
}
|
||||
|
||||
public destroyToolGroup(toolGroupId: string) {
|
||||
public destroyToolGroup(toolGroupId: string): void {
|
||||
ToolGroupManager.destroyToolGroup(toolGroupId);
|
||||
this.toolGroupIds.delete(toolGroupId);
|
||||
}
|
||||
@@ -222,6 +238,10 @@ export default class ToolGroupService {
|
||||
toolInstance.configuration = config;
|
||||
}
|
||||
|
||||
public getActivePrimaryMouseButtonTool(toolGroupId?: string): string {
|
||||
return this.getToolGroup(toolGroupId)?.getActivePrimaryMouseButtonTool();
|
||||
}
|
||||
|
||||
private _setToolsMode(toolGroup, tools) {
|
||||
const { active, passive, enabled, disabled } = tools;
|
||||
|
||||
@@ -279,4 +299,23 @@ export default class ToolGroupService {
|
||||
addTools(tools.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
private _onToolActivated = (evt: Types.EventTypes.ToolActivatedEventType) => {
|
||||
const { toolGroupId, toolName, toolBindingsOptions } = evt.detail;
|
||||
const isPrimaryTool = toolBindingsOptions.bindings?.some(
|
||||
binding => binding.mouseButton === Enums.MouseBindings.Primary
|
||||
);
|
||||
|
||||
const callbackProps = {
|
||||
toolGroupId,
|
||||
toolName,
|
||||
toolBindingsOptions,
|
||||
};
|
||||
|
||||
this._broadcastEvent(EVENTS.TOOL_ACTIVATED, callbackProps);
|
||||
|
||||
if (isPrimaryTool) {
|
||||
this._broadcastEvent(EVENTS.PRIMARY_TOOL_ACTIVATED, callbackProps);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PubSubService, ServicesManager } from '@ohif/core';
|
||||
import { PubSubService } from '@ohif/core';
|
||||
import * as OhifTypes from '@ohif/core/types';
|
||||
import {
|
||||
RenderingEngine,
|
||||
@@ -24,6 +24,7 @@ import JumpPresets from '../../utils/JumpPresets';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
||||
VIEWPORT_VOLUMES_CHANGED: 'event::cornerstoneViewportService:viewportVolumesChanged',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,14 +52,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
enableResizeDetector: true;
|
||||
resizeRefreshRateMs: 200;
|
||||
resizeRefreshMode: 'debounce';
|
||||
servicesManager = null;
|
||||
servicesManager: AppTypes.ServicesManager = null;
|
||||
|
||||
resizeQueue = [];
|
||||
viewportResizeTimer = null;
|
||||
gridResizeDelay = 50;
|
||||
gridResizeTimeOut = null;
|
||||
|
||||
constructor(servicesManager: ServicesManager) {
|
||||
constructor(servicesManager: AppTypes.ServicesManager) {
|
||||
super(EVENTS);
|
||||
this.renderingEngine = null;
|
||||
this.viewportGridResizeObserver = null;
|
||||
@@ -182,7 +183,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const { lutPresentation, positionPresentation } = presentations;
|
||||
if (lutPresentation) {
|
||||
const { presentation } = lutPresentation;
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
if (viewport instanceof BaseVolumeViewport) {
|
||||
if (presentation instanceof Map) {
|
||||
presentation.forEach((properties, volumeId) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
@@ -531,6 +532,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
cameraProps: unknown
|
||||
): string {
|
||||
const viewportInfo = this.getViewportInfo(activeViewportId);
|
||||
|
||||
if (viewportInfo.getViewportType() === csEnums.ViewportType.VOLUME_3D) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { referencedImageId } = cameraProps;
|
||||
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
|
||||
return activeViewportId;
|
||||
@@ -551,9 +557,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
): Promise<void> {
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
|
||||
const { imageIds, initialImageIndex, displaySetInstanceUID } = viewportData.data;
|
||||
const displaySetInstanceUIDs = viewportData.data.map(data => data.displaySetInstanceUID);
|
||||
|
||||
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
|
||||
// based on the cache service construct always the first one is the non-overlay
|
||||
// and the rest are overlays
|
||||
|
||||
this.viewportsDisplaySets.set(viewport.id, [...displaySetInstanceUIDs]);
|
||||
|
||||
const { initialImageIndex, imageIds } = viewportData.data[0];
|
||||
|
||||
let initialImageIndexToUse =
|
||||
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
|
||||
@@ -582,6 +593,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
}
|
||||
|
||||
this._handleOverlays(viewport);
|
||||
|
||||
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
||||
viewport.setProperties({ ...properties });
|
||||
this.setPresentations(viewport.id, presentations);
|
||||
@@ -720,10 +733,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
|
||||
const { displaySetService, toolGroupService } = this.servicesManager.services;
|
||||
const { displaySetService, toolGroupService, viewportGridService } =
|
||||
this.servicesManager.services;
|
||||
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
|
||||
const displaySetModality = displaySet?.Modality;
|
||||
|
||||
// Todo: use presentations states
|
||||
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
|
||||
@@ -749,7 +766,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
if (displayPreset !== undefined) {
|
||||
properties.preset = displayPreset;
|
||||
properties.preset = displayPreset[displaySetModality] || displayPreset.default;
|
||||
}
|
||||
|
||||
return { properties, volumeId };
|
||||
@@ -762,24 +779,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
this.setPresentations(viewport.id, presentations);
|
||||
|
||||
// load any secondary displaySets
|
||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||
|
||||
// can be SEG or RTSTRUCT for now
|
||||
const overlayDisplaySet = displaySetInstanceUIDs
|
||||
.map(displaySetService.getDisplaySetByUID)
|
||||
.find(displaySet => displaySet?.isOverlayDisplaySet);
|
||||
|
||||
if (overlayDisplaySet) {
|
||||
this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport);
|
||||
} else {
|
||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
||||
// and we can look into hydrated segmentations to check if any of them are
|
||||
// associated with the primary displaySet
|
||||
|
||||
// get segmentations only returns the hydrated segmentations
|
||||
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
|
||||
}
|
||||
this._handleOverlays(viewport);
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||
@@ -793,6 +793,32 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
viewport.render();
|
||||
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORT_VOLUMES_CHANGED, {
|
||||
viewportInfo,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleOverlays(viewport: Types.IStackViewport | Types.IVolumeViewport) {
|
||||
const { displaySetService } = this.servicesManager.services;
|
||||
|
||||
// load any secondary displaySets
|
||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||
|
||||
// can be SEG or RTSTRUCT for now
|
||||
const overlayDisplaySet = displaySetInstanceUIDs
|
||||
.map(displaySetService.getDisplaySetByUID)
|
||||
.find(displaySet => displaySet?.isOverlayDisplaySet);
|
||||
if (overlayDisplaySet) {
|
||||
this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport);
|
||||
} else {
|
||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
||||
// and we can look into hydrated segmentations to check if any of them are
|
||||
// associated with the primary displaySet
|
||||
|
||||
// get segmentations only returns the hydrated segmentations
|
||||
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
|
||||
}
|
||||
}
|
||||
|
||||
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
||||
@@ -1025,28 +1051,33 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
private performResize() {
|
||||
const isImmediate = false;
|
||||
|
||||
const viewports = this.getRenderingEngine().getViewports();
|
||||
try {
|
||||
const viewports = this.getRenderingEngine().getViewports();
|
||||
|
||||
// Store the current position presentations for each viewport.
|
||||
viewports.forEach(({ id }) => {
|
||||
const presentation = this.getPositionPresentation(id);
|
||||
this.beforeResizePositionPresentations.set(id, presentation);
|
||||
});
|
||||
// Store the current position presentations for each viewport.
|
||||
viewports.forEach(({ id }) => {
|
||||
const presentation = this.getPositionPresentation(id);
|
||||
this.beforeResizePositionPresentations.set(id, presentation);
|
||||
});
|
||||
|
||||
// Resize the rendering engine and render.
|
||||
const renderingEngine = this.renderingEngine;
|
||||
renderingEngine.resize(isImmediate);
|
||||
renderingEngine.render();
|
||||
// Resize the rendering engine and render.
|
||||
const renderingEngine = this.renderingEngine;
|
||||
renderingEngine.resize(isImmediate);
|
||||
renderingEngine.render();
|
||||
|
||||
// Reset the camera for viewports that should reset their camera on resize,
|
||||
// which means only those viewports that have a zoom level of 1.
|
||||
this.beforeResizePositionPresentations.forEach((positionPresentation, viewportId) => {
|
||||
this.setPresentations(viewportId, { positionPresentation });
|
||||
});
|
||||
// Reset the camera for viewports that should reset their camera on resize,
|
||||
// which means only those viewports that have a zoom level of 1.
|
||||
this.beforeResizePositionPresentations.forEach((positionPresentation, viewportId) => {
|
||||
this.setPresentations(viewportId, { positionPresentation });
|
||||
});
|
||||
|
||||
// Resize and render the rendering engine again.
|
||||
renderingEngine.resize(isImmediate);
|
||||
renderingEngine.render();
|
||||
// Resize and render the rendering engine again.
|
||||
renderingEngine.resize(isImmediate);
|
||||
renderingEngine.render();
|
||||
} catch (e) {
|
||||
// This can happen if the resize is too close to navigation or shutdown
|
||||
console.warn('Caught resize exception', e);
|
||||
}
|
||||
}
|
||||
|
||||
private resetGridResizeTimeout() {
|
||||
|
||||
Loaded 100 of 490 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user