Compare commits

..
152 Commits
Author SHA1 Message Date
ohif-bot 65c3719d1b chore(version): Update package versions [skip ci] 2024-12-03 14:33:02 +00:00
ohif-bot f9fdaee3b2 chore(version): version.json [skip ci] 2024-12-03 14:32:54 +00:00
e5286d9fc2 fix(modes): don't attempt to retrieve a stage index if HPs (#4552)
Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
Co-authored-by: Ștefan Silviu-Alexandru <silviu.stefan@atta.systems>
2024-12-03 09:14:41 -05:00
Alireza 71f06dcd3e fix(multiframe): metadata handling of NM studies and loading order (#4540) 2024-11-27 13:08:18 -05:00
ohif-bot 3b66867508 chore(version): Update package versions [skip ci] 2024-11-13 04:21:28 +00:00
ohif-bot 1d267e5576 chore(version): version.json [skip ci] 2024-11-13 04:21:19 +00:00
Alireza 610faa5a27 fix: video playback (#4497) 2024-11-12 21:18:06 -05:00
Dan Rukas 4f8281b04a fix(ui): Scrolling and display fixes for study panel (#4494) 2024-11-12 17:21:05 -05:00
Alireza 2f7bb02387 fix: publish version 3.9 (#4491) 2024-11-12 14:45:02 -05:00
ohif-bot 54d4eabf03 chore(version): Update package versions [skip ci] 2024-11-12 19:13:32 +00:00
ohif-bot 5c20bf0faf chore(version): version.json [skip ci] 2024-11-12 19:13:25 +00:00
Alireza b417063200 fix version json 2024-11-12 13:53:36 -05:00
Alireza 4bd343ab3e fix version json 2024-11-12 13:46:15 -05:00
Alireza a6de628132 publish version 3.9 2024-11-12 13:34:08 -05:00
ohif-bot f4775eccae chore(version): Update package versions [skip ci] 2024-11-12 18:31:31 +00:00
ohif-bot bf8fe7799c chore(version): version.json [skip ci] 2024-11-12 18:31:25 +00:00
Alireza 62b2748847 fix: Measurement Tracking: Various UI and functionality improvements (#4481) 2024-11-12 13:14:47 -05:00
Bill Wallace 4a125236dd fix: Have an addIcon that adds to both ui and ui-next (#4490) 2024-11-12 12:34:14 -05:00
Ibrahim a5f03764d1 fix(styles): several fixes for different styles (#4483) 2024-11-12 12:33:45 -05:00
Dan Rukas 1801a217e3 ui(components): updated component font sizes and design component docs (#4486) 2024-11-12 12:33:09 -05:00
ohif-bot 23a2582231 chore(version): Update package versions [skip ci] 2024-11-11 19:45:52 +00:00
ohif-bot 1105db5dd1 chore(version): version.json [skip ci] 2024-11-11 19:45:44 +00:00
Alireza 05d41c5206 fix(bugs): Update dependencies and enhance UI components (#4478) 2024-11-11 14:28:34 -05:00
ohif-bot 61e3a9cb52 chore(version): Update package versions [skip ci] 2024-11-08 15:51:44 +00:00
ohif-bot 4b69279c70 chore(version): version.json [skip ci] 2024-11-08 15:51:36 +00:00
Ibrahim 8fb8b3bfd1 fix(style): worklist shifting (#4477) 2024-11-08 10:34:22 -05:00
ohif-bot c1a196d802 chore(version): Update package versions [skip ci] 2024-11-07 05:37:54 +00:00
ohif-bot 4a564528ba chore(version): version.json [skip ci] 2024-11-07 05:37:46 +00:00
Alireza aa2b649444 fix(tmtv): fix toggle one up weird behaviours (#4473) 2024-11-07 00:20:30 -05:00
ohif-bot 3f7ae76113 chore(version): Update package versions [skip ci] 2024-11-06 16:36:09 +00:00
ohif-bot 43ad641cf1 chore(version): version.json [skip ci] 2024-11-06 16:36:00 +00:00
Leszek Grzanka c80cd6102d fix(JSONScript) update name of the script in the comments to follow renaming (#4441) 2024-11-06 11:19:20 -05:00
Alireza 3d11ef28f2 fix: build (#4471) 2024-11-06 11:16:43 -05:00
ohif-bot 69a81ae838 chore(version): Update package versions [skip ci] 2024-11-06 12:34:24 +00:00
ohif-bot 417785ecbc chore(version): version.json [skip ci] 2024-11-06 12:34:15 +00:00
Alireza 7ce7127d7c OHIF with Cornerstone3D 2.0 (#4347) 2024-11-06 07:15:27 -05:00
ohif-bot 8981a61b8c chore(version): Update package versions [skip ci] 2024-11-05 18:47:30 +00:00
ohif-bot c66c688ace chore(version): version.json [skip ci] 2024-11-05 18:47:21 +00:00
Dan Rukas d9450104af fix(ui) Study Browser small spacing changes (#4469) 2024-11-05 13:28:59 -05:00
ohif-bot 2835a555c8 chore(version): Update package versions [skip ci] 2024-10-30 14:34:08 +00:00
ohif-bot 41e8ee692b chore(version): version.json [skip ci] 2024-10-30 14:33:59 +00:00
Pedro H. Köhler 4f20523109 fix(ui): show ui notification on displaySet load error (#4447) 2024-10-30 10:15:48 -04:00
ohif-bot 0166956dd8 chore(version): Update package versions [skip ci] 2024-10-29 23:31:44 +00:00
ohif-bot 1aa2c646a6 chore(version): version.json [skip ci] 2024-10-29 23:31:37 +00:00
Pedro H. Köhler 2e96371b06 fix(ui): display error in ui while loading seg (#4433) 2024-10-29 19:14:34 -04:00
ohif-bot 543c31109b chore(version): Update package versions [skip ci] 2024-10-29 14:10:39 +00:00
ohif-bot 2d1704063c chore(version): version.json [skip ci] 2024-10-29 14:10:31 +00:00
Pedro H. Köhler 0610425740 fix(packages): http-proxy-middleware vulnerability (#4443) 2024-10-29 09:52:07 -04:00
ohif-bot ce9ff7d535 chore(version): Update package versions [skip ci] 2024-10-18 15:59:40 +00:00
ohif-bot b4918d8a33 chore(version): version.json [skip ci] 2024-10-18 15:59:32 +00:00
Ibrahim a7fd9fa5bf feat(new-study-panel): default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser (#4417) 2024-10-18 11:42:44 -04:00
ohif-bot 1d0259451f chore(version): Update package versions [skip ci] 2024-10-17 16:39:04 +00:00
ohif-bot abf2093d30 chore(version): version.json [skip ci] 2024-10-17 16:38:55 +00:00
dependabot[bot] 8fc395e68f chore(deps): bump webpack from 5.89.0 to 5.94.0 (#4354)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 12:20:33 -04:00
Ibrahim 33af9bb021 fix(tmtv): prevent fusion row in tmtv from getting inverted unexpectedly (#4420) 2024-10-17 12:18:15 -04:00
ohif-bot 8dab0883c8 chore(version): Update package versions [skip ci] 2024-10-17 14:20:54 +00:00
ohif-bot a146c9a50e chore(version): version.json [skip ci] 2024-10-17 14:20:44 +00:00
BillucandLuc Billaud 2ec4692eaf fix(createReport): early return on cancel in prompt (#4243)
Co-authored-by: Luc Billaud <luc.billaud@creatis.insa-lyon.fr>
2024-10-17 09:49:53 -04:00
Ibrahim 03aad4eba2 fix(typo): type in fourup preset (#4426) 2024-10-17 09:47:37 -04:00
dependabot[bot] f14aee2733 chore(deps): bump micromatch from 4.0.7 to 4.0.8 (#4350)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 09:46:36 -04:00
dependabot[bot] 57eb829173 chore(deps): bump express from 4.19.2 to 4.21.1 (#4422)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-17 09:44:37 -04:00
Ibrahim ace67b3bbb fix(hp-presets): select the active displaySet when toggling an HP preset, not a random one from the series panel (#4365) 2024-10-17 09:43:48 -04:00
Ibrahim 307b14476b fix(tools): check if seriesNumber is an undefined properly (#4338) 2024-10-17 09:43:10 -04:00
Ibrahim 433cc8089d fix(3d-viewport): exception was being thrown and 3d viewpot not getting resized. (#4366) 2024-10-17 09:42:05 -04:00
Sofien-Sellami fa2435d5e9 feat(hangingProtocols): added selection of the HangingProtocol stage from the url (#4310) 2024-10-17 09:41:41 -04:00
Ceciliadrc e8f9345b93 feat(removeButton) : add remove button from toolbar option (#4408) 2024-10-17 09:40:53 -04:00
brandon flowers e254e7bb3d chore(ui): Added Data Test Ids to Cine controls for e2e Testing (#4415) 2024-10-17 09:40:24 -04:00
Joeycho 7353f7f069 fix(sr): load existing point, if there is 2nd point in renderableData (Fix rotation in arrow annotation) (#4356) 2024-10-17 09:39:48 -04:00
Igor Octaviano 3dd0666c0c fix(dicomjson): Update getUIDsFromImageID to work with json data source + update getDisplaySetImageUIDs to work with mixed sop class json (#4322) 2024-10-17 09:38:40 -04:00
Ceciliadrc d5bcf54e23 fix(updateIndex): getNumberOfSlices is defined when used with a 3D viewport (#4424) 2024-10-17 09:36:42 -04:00
Igor Octaviano ac1cad25af feat(SR): SCOORD3D point annotations support for stack viewports (#4315) 2024-10-17 09:36:24 -04:00
Dan Rukas e0fca8c88c ui(component): Added new Toast (Sonner) component (#4412) 2024-10-17 09:30:42 -04:00
ohif-bot 43f54aa49c chore(version): Update package versions [skip ci] 2024-10-15 14:10:16 +00:00
ohif-bot 9b2e98a760 chore(version): version.json [skip ci] 2024-10-15 14:10:08 +00:00
Alireza 465df5c573 docs(ui): add base for ui component showcase in docs (#4414) 2024-10-15 09:53:26 -04:00
ohif-bot 9125cec333 chore(version): Update package versions [skip ci] 2024-10-11 13:25:01 +00:00
ohif-bot 87d623a76e chore(version): version.json [skip ci] 2024-10-11 13:24:54 +00:00
James PettsandJames Petts e849199eb0 fix(auth): oidc-react-issue (#4410)
Co-authored-by: James Petts <jamespetts@Yunu-MacBookPro-JamesPetts.local>
2024-10-11 09:09:26 -04:00
ohif-bot 0495020389 chore(version): Update package versions [skip ci] 2024-10-10 15:28:10 +00:00
ohif-bot 98ab95a743 chore(version): version.json [skip ci] 2024-10-10 15:28:02 +00:00
Ibrahim ec685ef5b9 fix(fossa): update fossa to track licenses correctly (#4411) 2024-10-10 11:12:21 -04:00
ohif-bot 70eb2aa6d7 chore(version): Update package versions [skip ci] 2024-10-08 16:41:57 +00:00
ohif-bot a1597d7bd6 chore(version): version.json [skip ci] 2024-10-08 16:41:49 +00:00
Dan Rukas 8f0035aeef ui(component): Adds flexible DataRow component (#4399) 2024-10-08 12:25:44 -04:00
ohif-bot c8923c5499 chore(version): Update package versions [skip ci] 2024-10-04 18:02:36 +00:00
ohif-bot c0c4f2398c chore(version): version.json [skip ci] 2024-10-04 18:02:29 +00:00
Ibrahim 60a8d5154a feat(tours): freeze versions and add licensings doc (#4407) 2024-10-04 13:47:32 -04:00
ohif-bot efc554bd49 chore(version): Update package versions [skip ci] 2024-10-04 17:21:26 +00:00
ohif-bot fc5f73bdfd chore(version): version.json [skip ci] 2024-10-04 17:21:18 +00:00
Ibrahim 579a592812 Merge branch 'IbrahimCSAE-feat/interactive-onboarding-guides' 2024-10-04 12:53:33 -04:00
Ibrahim ea961d6b66 add demo 2024-10-04 12:42:19 -04:00
Ibrahim 69a620ac68 read me 2024-10-04 12:37:10 -04:00
Ibrahim 8eaebd015d updates 2024-10-04 12:28:26 -04:00
Ibrahim 0326496c47 updates 2024-10-04 12:27:43 -04:00
Ibrahim 327f2cb1cf update 2024-10-04 12:16:56 -04:00
Ibrahim 478def8447 updates 2024-10-04 12:16:01 -04:00
Ibrahim 0a9cbb7589 updates 2024-10-04 12:14:31 -04:00
Ibrahim c5752e756a updates 2024-10-04 10:03:20 -04:00
Ibrahim 3b20d2b816 updates 2024-10-04 08:27:40 -04:00
Ibrahim 0d4cb69ec3 styling 2024-10-04 08:18:20 -04:00
ohif-bot 4f18a21868 chore(version): Update package versions [skip ci] 2024-10-01 20:56:33 +00:00
ohif-bot c4299de984 chore(version): version.json [skip ci] 2024-10-01 20:56:25 +00:00
Pedro H. Köhler a11cd6d6cb fix(Select): select clear button (#4398) 2024-10-01 16:42:21 -04:00
ohif-bot a72192da28 chore(version): Update package versions [skip ci] 2024-10-01 12:49:31 +00:00
ohif-bot 0dca891ff3 chore(version): version.json [skip ci] 2024-10-01 12:49:22 +00:00
Pedro H. Köhler b4f08adfb6 fix(dependent): high priority vulnerabilities (#4396) 2024-10-01 08:20:19 -04:00
ohif-bot 4d87e4dbda chore(version): Update package versions [skip ci] 2024-09-30 22:00:24 +00:00
ohif-bot 6a02f0b41c chore(version): version.json [skip ci] 2024-09-30 22:00:16 +00:00
Igor Octaviano 43efed207e fix: 🐛 Fix imports for ui-next (#4394) 2024-09-30 17:45:14 -04:00
Ibrahim 4f2583483c updates 2024-09-27 16:38:07 -04:00
Ibrahim e3f7838ce0 updates 2024-09-27 16:27:23 -04:00
Ibrahim 08d329add4 updates 2024-09-27 16:14:14 -04:00
Ibrahim 579c0c9d01 updates 2024-09-27 15:44:01 -04:00
Ibrahim ea7a3e07cb u 2024-09-27 12:58:48 -04:00
Ibrahim e91811ff23 updates 2024-09-27 12:58:39 -04:00
ohif-bot dd8e69a8cf chore(version): Update package versions [skip ci] 2024-09-27 16:33:00 +00:00
ohif-bot b1b66076b9 chore(version): version.json [skip ci] 2024-09-27 16:32:51 +00:00
Dan Rukas 80b1f54af1 ui(dropdown menu): Added DropdownMenu component in ui-next (#4387) 2024-09-27 12:17:54 -04:00
ohif-bot 75fde87b59 chore(version): Update package versions [skip ci] 2024-09-24 19:59:31 +00:00
ohif-bot 64f2f50f63 chore(version): version.json [skip ci] 2024-09-24 19:59:24 +00:00
Dan Rukas 7f17d5d733 ui(segmentation): Design Prototype for split panel component (#4378) 2024-09-24 15:44:59 -04:00
Ibrahim 9ff4ece541 updates 2024-09-22 20:47:33 -04:00
Ibrahim e534802abb updates 2024-09-22 20:41:12 -04:00
Ibrahim 51a87028c1 updates 2024-09-22 20:30:07 -04:00
Ibrahim eba85aa5b3 updates 2024-09-22 20:30:00 -04:00
Ibrahim 331778dfef Merge branch 'feat/interactive-onboarding-guides' of https://github.com/IbrahimCSAE/Viewers into local-interactive-onboarding-guides 2024-09-20 11:44:43 -04:00
Ibrahim daa5a857e4 updates 2024-09-20 11:44:36 -04:00
ohif-bot cb1f3b877b chore(version): Update package versions [skip ci] 2024-09-19 16:23:16 +00:00
ohif-bot f748d2a73a chore(version): version.json [skip ci] 2024-09-19 16:23:09 +00:00
Dan Rukas 365d824b98 fix(ui): Fixed study component open and closed feedback in Studies panel (#4384) 2024-09-19 12:07:54 -04:00
ohif-bot ee8962e9a7 chore(version): Update package versions [skip ci] 2024-09-19 04:39:08 +00:00
ohif-bot cde457c626 chore(version): version.json [skip ci] 2024-09-19 04:39:01 +00:00
Alireza 5ef102681d refactor(config): Update AWS S3 Static wado server URLs (#4381) 2024-09-19 00:23:28 -04:00
ohif-bot d55e794a86 chore(version): Update package versions [skip ci] 2024-09-17 04:08:08 +00:00
ohif-bot 4f7fa35def chore(version): version.json [skip ci] 2024-09-17 04:08:01 +00:00
Ibrahim 1f09527ae6 docs(mobile): IOS & Android Debugging Guides (#4371) 2024-09-16 23:53:10 -04:00
Ibrahim 118922f365 progress 2024-09-13 11:01:41 -04:00
ohif-bot 926f549e91 chore(version): Update package versions [skip ci] 2024-09-12 18:49:56 +00:00
ohif-bot 0d900bf4ad chore(version): version.json [skip ci] 2024-09-12 18:49:48 +00:00
Pedro H. Köhler 1bfce0a03c feat(toolbar): enable extensions to change toolbar button sections (#4367) 2024-09-12 14:34:25 -04:00
ohif-bot c08debd65a chore(version): Update package versions [skip ci] 2024-09-11 19:13:10 +00:00
ohif-bot ab5cead2af chore(version): version.json [skip ci] 2024-09-11 19:13:02 +00:00
Ibrahim 7a96406a11 feat(studies-panel): New OHIF study panel - under experimental flag (#4254) 2024-09-11 14:57:19 -04:00
ohif-bot 4432b6b420 chore(version): Update package versions [skip ci] 2024-09-05 13:31:46 +00:00
ohif-bot 7670941256 chore(version): version.json [skip ci] 2024-09-05 13:31:38 +00:00
Bill Wallace e1feffa425 fix: Add kheops integration into OHIF v3 again (#4345) 2024-09-05 09:15:48 -04:00
ohif-bot 3b47e6971c chore(version): Update package versions [skip ci] 2024-08-27 12:05:44 +00:00
ohif-bot 13ea778e61 chore(version): version.json [skip ci] 2024-08-27 12:05:36 +00:00
Igor Octaviano 14498d4e9a fix: 🐛 SeriesInstanceUID fallback + update retrieve metadata filtered to check for lazy (#4346) 2024-08-27 07:49:48 -04:00
ohif-bot 83f3bf4ec1 chore(version): Update package versions [skip ci] 2024-08-16 05:35:24 +00:00
ohif-bot 3b96c9e771 chore(version): version.json [skip ci] 2024-08-16 05:35:17 +00:00
dependabot[bot] da8bdb94a9 chore(deps): bump axios from 0.25.0 to 0.28.1 (#4337)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-16 01:09:56 -04:00
1273 changed files with 57630 additions and 18792 deletions

No files matched your search

-8
View File
@@ -180,10 +180,6 @@ jobs:
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
@@ -230,10 +226,6 @@ jobs:
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
@@ -5,8 +5,6 @@ server {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -7,8 +7,6 @@ server {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
+5 -1
View File
@@ -8,6 +8,8 @@ jobs:
playwright-tests:
timeout-minutes: 60
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.49.0-noble
strategy:
fail-fast: false
matrix:
@@ -23,7 +25,9 @@ jobs:
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: export NODE_OPTIONS="--max_old_space_size=8192" && npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run:
export NODE_OPTIONS="--max_old_space_size=8192" && npx playwright test --shard=${{
matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
+2 -2
View File
@@ -4,7 +4,7 @@
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
* in the DICOM JSON Data source on docs.ohif.org
*
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
* Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
*
* params:
* - studyFolder: path to the study folder which contains the DICOM files
@@ -30,7 +30,7 @@ const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args;
if (args.length < 3 || args.length > 4) {
console.error(
'Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
'Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
);
process.exit(1);
}
+18 -24
View File
@@ -22,7 +22,6 @@ const cssToJavaScript = require('./rules/cssToJavaScript.js');
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
@@ -58,10 +57,6 @@ if (!process.env.APP_CONFIG) {
}
module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
if (!process.env.NODE_ENV) {
throw new Error('process.env.NODE_ENV not set');
}
const mode = NODE_ENV === 'production' ? 'production' : 'development';
const isProdBuild = NODE_ENV === 'production';
const isQuickBuild = QUICK_BUILD === 'true';
@@ -99,16 +94,20 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
type: 'filesystem',
},
module: {
noParse: [/(codec)/, /(dicomicc)/],
noParse: [/(dicomicc)/],
rules: [
...(isProdBuild ? [] : [{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: ['react-refresh/babel'],
},
}]),
...(isProdBuild
? []
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
]),
{
test: /\.svg?$/,
oneOf: [
@@ -123,11 +122,11 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
removeViewBox: false,
},
},
},
]
],
},
prettier: false,
svgo: true,
@@ -141,11 +140,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
},
],
},
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode),
loadWebWorkersRule,
// loadShadersRule,
@@ -185,8 +179,8 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
'@cornerstonejs/dicom-image-loader':
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
'dicom-microscopy-viewer':
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
},
// Which directories to search when resolving modules
modules: [
@@ -214,7 +208,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin()]),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
+370
View File
@@ -3,6 +3,376 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **modes:** don't attempt to retrieve a stage index if HPs ([#4552](https://github.com/OHIF/Viewers/issues/4552)) ([e5286d9](https://github.com/OHIF/Viewers/commit/e5286d9fc2449da50b738e7e6208d489cce1633e))
* **multiframe:** metadata handling of NM studies and loading order ([#4540](https://github.com/OHIF/Viewers/issues/4540)) ([71f06dc](https://github.com/OHIF/Viewers/commit/71f06dcd3e6d24f1e3d931f7db756c26038927da))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
### Bug Fixes
* publish version 3.9 ([#4491](https://github.com/OHIF/Viewers/issues/4491)) ([2f7bb02](https://github.com/OHIF/Viewers/commit/2f7bb02387a19e953e5f09aad06b72d96cffcbd1))
* **ui:** Scrolling and display fixes for study panel ([#4494](https://github.com/OHIF/Viewers/issues/4494)) ([4f8281b](https://github.com/OHIF/Viewers/commit/4f8281b04a1eae6890e7ecd94f8a61c281217d3f))
* video playback ([#4497](https://github.com/OHIF/Viewers/issues/4497)) ([610faa5](https://github.com/OHIF/Viewers/commit/610faa5a2738da5eabc40e57e338c359f481e852))
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Have an addIcon that adds to both ui and ui-next ([#4490](https://github.com/OHIF/Viewers/issues/4490)) ([4a12523](https://github.com/OHIF/Viewers/commit/4a125236ddbf8a4a95fb9c5820f511d0224e663f))
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
* **styles:** several fixes for different styles ([#4483](https://github.com/OHIF/Viewers/issues/4483)) ([a5f0376](https://github.com/OHIF/Viewers/commit/a5f03764d1fe07b55635c52c5dac38ab5e694ba1))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
### Bug Fixes
* **style:** worklist shifting ([#4477](https://github.com/OHIF/Viewers/issues/4477)) ([8fb8b3b](https://github.com/OHIF/Viewers/commit/8fb8b3bfd1c887cd67fc058629d7aba598c76f9e))
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
### Bug Fixes
* **ui:** show ui notification on displaySet load error ([#4447](https://github.com/OHIF/Viewers/issues/4447)) ([4f20523](https://github.com/OHIF/Viewers/commit/4f20523109ecbb7ec5a6d5f2c97f7e73f81cda09))
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
### Bug Fixes
* **ui:** display error in ui while loading seg ([#4433](https://github.com/OHIF/Viewers/issues/4433)) ([2e96371](https://github.com/OHIF/Viewers/commit/2e96371b0631a9e5d411b0142300708ab8ba7d27))
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
### Bug Fixes
* **packages:** http-proxy-middleware vulnerability ([#4443](https://github.com/OHIF/Viewers/issues/4443)) ([0610425](https://github.com/OHIF/Viewers/commit/06104257402e872d447e59cb166184d9a3548f8b))
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
### Features
* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f))
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
### Bug Fixes
* **tmtv:** prevent fusion row in tmtv from getting inverted unexpectedly ([#4420](https://github.com/OHIF/Viewers/issues/4420)) ([33af9bb](https://github.com/OHIF/Viewers/commit/33af9bb021ff3a6c3b683d4df2c730413400ff8a))
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Bug Fixes
* **3d-viewport:** exception was being thrown and 3d viewpot not getting resized. ([#4366](https://github.com/OHIF/Viewers/issues/4366)) ([433cc80](https://github.com/OHIF/Viewers/commit/433cc8089db6aa218c9075bd0eeb7952a7e4f028))
* **createReport:** early return on cancel in prompt ([#4243](https://github.com/OHIF/Viewers/issues/4243)) ([2ec4692](https://github.com/OHIF/Viewers/commit/2ec4692eaf2349e21b141a2c0b5b104ee10f7a28))
* **dicomjson:** Update getUIDsFromImageID to work with json data source + update getDisplaySetImageUIDs to work with mixed sop class json ([#4322](https://github.com/OHIF/Viewers/issues/4322)) ([3dd0666](https://github.com/OHIF/Viewers/commit/3dd0666c0c090cbd66161f24bc9795f96abb3697))
* **hp-presets:** select the active displaySet when toggling an HP preset, not a random one from the series panel ([#4365](https://github.com/OHIF/Viewers/issues/4365)) ([ace67b3](https://github.com/OHIF/Viewers/commit/ace67b3bbb6be4e8c78e613e20d3e10b93762bf7))
* **sr:** load existing point, if there is 2nd point in renderableData (Fix rotation in arrow annotation) ([#4356](https://github.com/OHIF/Viewers/issues/4356)) ([7353f7f](https://github.com/OHIF/Viewers/commit/7353f7f069446f8484278c2cff5b09149cfa23eb))
* **tools:** check if seriesNumber is an undefined properly ([#4338](https://github.com/OHIF/Viewers/issues/4338)) ([307b144](https://github.com/OHIF/Viewers/commit/307b14476be41b10b861d6a8474f7386b5107618))
* **typo:** type in fourup preset ([#4426](https://github.com/OHIF/Viewers/issues/4426)) ([03aad4e](https://github.com/OHIF/Viewers/commit/03aad4eba24e33a266a6d91eaf74df52dc2a550e))
* **updateIndex:** getNumberOfSlices is defined when used with a 3D viewport ([#4424](https://github.com/OHIF/Viewers/issues/4424)) ([d5bcf54](https://github.com/OHIF/Viewers/commit/d5bcf54e23ef68abd85c5f0ea671feca637c4f49))
### Features
* **hangingProtocols:** added selection of the HangingProtocol stage from the url ([#4310](https://github.com/OHIF/Viewers/issues/4310)) ([fa2435d](https://github.com/OHIF/Viewers/commit/fa2435d5e94e5f903404ca94687b086f90f8d1f8))
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
### Bug Fixes
* **auth:** oidc-react-issue ([#4410](https://github.com/OHIF/Viewers/issues/4410)) ([e849199](https://github.com/OHIF/Viewers/commit/e849199eb0a9ecba4f9845aa1e07df775d5ded9b))
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
### Bug Fixes
* **fossa:** update fossa to track licenses correctly ([#4411](https://github.com/OHIF/Viewers/issues/4411)) ([ec685ef](https://github.com/OHIF/Viewers/commit/ec685ef5b9c8bfa5bff3bbf869eb394548ae1cab))
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
### Features
* **tours:** freeze versions and add licensings doc ([#4407](https://github.com/OHIF/Viewers/issues/4407)) ([60a8d51](https://github.com/OHIF/Viewers/commit/60a8d5154a5d6d2b121bd93aeacf12d97ef9f8cb))
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
### Bug Fixes
* **Select:** select clear button ([#4398](https://github.com/OHIF/Viewers/issues/4398)) ([a11cd6d](https://github.com/OHIF/Viewers/commit/a11cd6d6cbe20d7d986430befb3398f910a03ada))
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
### Bug Fixes
* **dependent:** high priority vulnerabilities ([#4396](https://github.com/OHIF/Viewers/issues/4396)) ([b4f08ad](https://github.com/OHIF/Viewers/commit/b4f08adfb638e5df11bb77d3c1b128b5efdf77a7))
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
### Bug Fixes
* 🐛 Fix imports for ui-next ([#4394](https://github.com/OHIF/Viewers/issues/4394)) ([43efed2](https://github.com/OHIF/Viewers/commit/43efed207e0d8d13bcbf52fab14c1be034d22d0c))
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
### Bug Fixes
* **ui:** Fixed study component open and closed feedback in Studies panel ([#4384](https://github.com/OHIF/Viewers/issues/4384)) ([365d824](https://github.com/OHIF/Viewers/commit/365d824b98e03b87db294878abde6823abdcf409))
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
### Features
* **toolbar:** enable extensions to change toolbar button sections ([#4367](https://github.com/OHIF/Viewers/issues/4367)) ([1bfce0a](https://github.com/OHIF/Viewers/commit/1bfce0a03cbbb4cc1f69e8b5d1d72244b30d6b46))
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
### Features
* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58))
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
### Bug Fixes
* Add kheops integration into OHIF v3 again ([#4345](https://github.com/OHIF/Viewers/issues/4345)) ([e1feffa](https://github.com/OHIF/Viewers/commit/e1feffa42553d6c8650a4aceb09f72c637126660))
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
### Bug Fixes
* 🐛 SeriesInstanceUID fallback + update retrieve metadata filtered to check for lazy ([#4346](https://github.com/OHIF/Viewers/issues/4346)) ([14498d4](https://github.com/OHIF/Viewers/commit/14498d4e9a6a57324b8be9f0b314f2901459dc4a))
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package ohif-monorepo-root
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package ohif-monorepo-root
+148
View File
@@ -10,6 +10,7 @@ Please find below the list of datasets used on the demo page, along with their r
## Platforms
### NIH NCI IDC
- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188.
@@ -147,3 +148,150 @@ Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU)
### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2
https://data.kitware.com/
### 1.2.276.0.7230010.3.1.2.296485376.1.1665793212.499772
### 2.25.269859997690759739055099378767846712697
### 1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.232252967813565730694525674696
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
### 1.3.6.1.4.1.5962.99.1.1117.5035.1620319789811.1.2.1
### 1.3.6.1.4.1.5962.99.1.1123.9231.1620326176300.1.2.1
### 1.3.6.1.4.1.5962.99.1.1126.3483.1620329455972.1.2.1
https://github.com/ImagingInformatics/hackathon-images
### 2.16.124.113543.6004.101.103.20021117.162333.1
### 2.16.124.113543.6004.101.103.20021117.190619.1
### 2.16.124.113543.6004.101.103.20021117.123455.1
### 2.16.124.113543.6004.101.103.20021117.061159.1
https://www.aapm.org/
### 1.2.840.113619.2.30.1.1762295590.1623.978668949.886
### 1.2.276.0.7230010.3.1.2.447481088.1.1669202398.851612
Custom data SPECT, specifically I123-FP-CIT (DaTSCAN) SPECT, evaluates the dopaminergic system to diagnose Parkinson's disease, especially when tremor symptoms are unclear. It helps distinguish Parkinson's disease from treatment-related tremor.
### 1.3.6.1.4.1.9328.50.1.54652
https://www.cancerimagingarchive.net/collection/rider-pilot/
Lung Image Database Consortium (LIDC). (2023) RIDER Pilot [Data set]. The Cancer Imaging Archive (TCIA). https://doi.org/10.7937/m87f-mz83
### 1.3.6.1.4.1.14519.5.2.1.331759366792756327296606233801322964986
Mayr, N., Yuh, W. T. C., Bowen, S., Harkenrider, M., Knopp, M. V., Lee, E. Y.-P., Leung, E., Lo, S. S., Small Jr., W., & Wolfson, A. H. (2023). Cervical Cancer – Tumor Heterogeneity: Serial Functional and Molecular Imaging Across the Radiation Therapy Course in Advanced Cervical Cancer (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/ERZ5-QZ59
https://www.cancerimagingarchive.net/collection/cc-tumor-heterogeneity/
### 1.3.6.1.4.1.14519.5.2.1.297577087050970310787702792940607009472
Eslick, E. M., Kipritidis, J., Gradinscak, D., Stevens, M. J., Bailey, D. L., Harris, B., Booth, J. T., & Keall, P. J. (2022). CT Ventilation as a functional imaging modality for lung cancer radiotherapy (CT-vs-PET-Ventilation-Imaging) (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/3ppx-7s22
https://www.cancerimagingarchive.net/collection/ct-vs-pet-ventilation-imaging/
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.634114621738943599785009586807
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.135953723682765205394176991681
Huang, W., Tudorica, A., Chui, S., Kemmer, K., Naik, A., Troxell, M., Oh, K., Roy, N., Afzal, A., & Holtorf, M. (2014). Variations of dynamic contrast-enhanced magnetic resonance imaging in evaluation of breast cancer therapy response: a multicenter data analysis challenge (QIN Breast DCE-MRI) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2014.a2n1ixox
https://www.cancerimagingarchive.net/collection/qin-breast-dce-mri/
### 1.3.6.1.4.1.14519.5.2.1.1.24766180081901755714059656629507905556
Cancer Moonshot Biobank. (2023). Cancer Moonshoot Biobank – Acute Myeloid Leukemia (CMB-AML) (Version 4) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/PCTE-6M66
https://www.cancerimagingarchive.net/collection/cmb-aml/
### 1.3.6.1.4.1.14519.5.2.1.3098.5025.285242291560760827564488897577
https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/
Madhavi, P., Patel, S., & Tsao, A. S. (2019). Data from Anti-PD-1 Immunotherapy Lung [Data set]. The Cancer Imaging Archive. DOI: 10.7937/tcia.2019.zjjwb9ip
### 1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384
https://www.cancerimagingarchive.net/collection/cmb-pca/
Cancer Moonshot Biobank. (2022). Cancer Moonshot Biobank – Prostate Cancer Collection (CMB-PCA) (Version 7) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/25T7-6Y12
### 1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331
Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
https://www.cancerimagingarchive.net/collection/nsclc-radiomics/
### 1.3.6.1.4.1.14519.5.2.1.7085.2626.494695569589117268722281491772
https://www.cancerimagingarchive.net/collection/cptac-ucec/
National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2019). The Clinical Proteomic Tumor Analysis Consortium Uterine Corpus Endometrial Carcinoma Collection (CPTAC-UCEC) (Version 12) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.3R3JUISW
### 1.3.6.1.4.1.14519.5.2.1.207544490797667703011829289839681390478
https://www.cancerimagingarchive.net/collection/remind/
Juvekar, P., Dorent, R., Kögl, F., Torio, E., Barr, C., Rigolo, L., Galvin, C., Jowkar, N., Kazi, A., Haouchine, N., Cheema, H., Navab, N., Pieper, S., Wells, W. M., Bi, W. L., Golby, A., Frisken, S., & Kapur, T. (2023). The Brain Resection Multimodal Imaging Database (ReMIND) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/3RAG-D070
### 1.3.12.2.1107.5.1.4.60175.30000008042114404745300000010
Gavrielides, M. A., Kinnard, L. M., Myers, K. J., Peregoy, J., Pritchard, W. F., Zeng, R., Esparza, J., Karanian, J., & Petrick, N. (2015). Data From Phantom FDA [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/TCIA.2015.orbjkmux
https://www.cancerimagingarchive.net/collection/phantom-fda/
### 1.3.6.1.4.1.14519.5.2.1.6834.5010.992793141464713669479982159310
https://www.cancerimagingarchive.net/collection/4d-lung/
Hugo, G. D., Weiss, E., Sleeman, W. C., Balik, S., Keall, P. J., Lu, J., & Williamson, J. F. (2016). Data from 4D Lung Imaging of NSCLC Patients (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.ELN8YGLE
### 1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036
https://www.cancerimagingarchive.net/collection/rider-lung-pet-ct/
Muzi P, Wanner M, & Kinahan P. (2015). Data From RIDER Lung PET-CT. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2015.ofip7tvm
### 1.3.6.1.4.1.14519.5.2.1.9823.1001.134394060407147891170882809392
https://www.cancerimagingarchive.net/collection/prostate-mri/
Choyke P, Turkbey B, Pinto P, Merino M, Wood B. (2016). Data From PROSTATE-MRI. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2016.6046GUDv
### 1.3.6.1.4.1.14519.5.2.1.191696062987463500085282581898315738844
https://www.cancerimagingarchive.net/collection/upenn-gbm/
Bakas, S., Sako, C., Akbari, H., Bilello, M., Sotiras, A., Shukla, G., Rudie, J. D., Flores Santamaria, N., Fathi Kazerooni, A., Pati, S., Rathore, S., Mamourian, E., Ha, S. M., Parker, W., Doshi, J., Baid, U., Bergman, M., Binder, Z. A., Verma, R., … Davatzikos, C. (2021). Multi-parametric magnetic resonance imaging (mpMRI) scans for de novo Glioblastoma (GBM) patients from the University of Pennsylvania Health System (UPENN-GBM) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.709X-DN49
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481
https://www.cancerimagingarchive.net/collection/breast-diagnosis/
Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
### 1.3.6.1.4.1.14519.5.2.1.1620.1225.189514895974227080410265976065
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
https://www.cancerimagingarchive.net/collection/ea1141/
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
From OFFIS DICOM-Team
https://www.offis.de/
OFFIS DICOM-Team
+7 -9
View File
@@ -142,13 +142,11 @@ Here is a schematic representation of our development workflow:
### Getting Started
1. [Fork this repository][how-to-fork]
2. [Clone your forked repository][how-to-clone]
- `git clone https://github.com/YOUR-USERNAME/Viewers.git`
3. Navigate to the cloned project's directory
4. Add this repo as a `remote` named `upstream`
- `git remote add upstream https://github.com/OHIF/Viewers.git`
5. `yarn install` to restore dependencies and link projects
1. Clone the repository
- `git clone https://github.com/OHIF/Viewers.git`
2. Navigate to the cloned project's directory
3. `yarn install` to restore dependencies and link projects
4. `yarn dev` to start the development server
#### To Develop
@@ -168,7 +166,7 @@ These commands are available from the root directory. Each project directory
also supports a number of commands that can be found in their respective
`README.md` and `package.json` files.
| Yarn Commands | Description |
| Commands | Description |
| ---------------------------- | ------------------------------------------------------------- |
| **Develop** | |
| `dev` or `start` | Default development experience for Viewer |
@@ -333,4 +331,4 @@ MIT © [OHIF](https://github.com/OHIF)
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
<!-- prettier-ignore-end -->
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license)
+283
View File
@@ -3,6 +3,289 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **modes:** don't attempt to retrieve a stage index if HPs ([#4552](https://github.com/OHIF/Viewers/issues/4552)) ([e5286d9](https://github.com/OHIF/Viewers/commit/e5286d9fc2449da50b738e7e6208d489cce1633e))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @externals/devDependencies
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"license": "MIT",
"private": true,
"engines": {
@@ -10,7 +10,7 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "30.4.1",
"@kitware/vtk.js": "32.1.1",
"clsx": "^2.1.1",
"core-js": "^3.2.1",
"moment": "^2.9.4"
@@ -77,9 +77,9 @@
"stylus": "^0.59.0",
"stylus-loader": "^7.1.3",
"terser-webpack-plugin": "^5.1.4",
"typescript": "4.6.4",
"typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "5.89.0",
"webpack": "5.94.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "4.7.3",
+280
View File
@@ -3,6 +3,286 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1"
+1 -1
View File
@@ -45,7 +45,7 @@
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
"typescript": "4.6.4",
"typescript": "5.5.4",
"sharp": "^0.32.6"
}
}
+1 -1
View File
@@ -1 +1 @@
b2c0faf29df81898f4ea250c7cade2bd4849e367
e5286d9fc2449da50b738e7e6208d489cce1633e
@@ -3,6 +3,308 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **modes:** don't attempt to retrieve a stage index if HPs ([#4552](https://github.com/OHIF/Viewers/issues/4552)) ([e5286d9](https://github.com/OHIF/Viewers/commit/e5286d9fc2449da50b738e7e6208d489cce1633e))
* **multiframe:** metadata handling of NM studies and loading order ([#4540](https://github.com/OHIF/Viewers/issues/4540)) ([71f06dc](https://github.com/OHIF/Viewers/commit/71f06dcd3e6d24f1e3d931f7db756c26038927da))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
### Bug Fixes
* video playback ([#4497](https://github.com/OHIF/Viewers/issues/4497)) ([610faa5](https://github.com/OHIF/Viewers/commit/610faa5a2738da5eabc40e57e338c359f481e852))
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Features
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.79",
"@ohif/extension-cornerstone": "3.9.0-beta.79",
"@ohif/extension-default": "3.9.0-beta.79",
"@ohif/i18n": "3.9.0-beta.79",
"@ohif/core": "3.9.2",
"@ohif/extension-cornerstone": "3.9.2",
"@ohif/extension-default": "3.9.2",
"@ohif/i18n": "3.9.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.77.6",
"@cornerstonejs/core": "^1.77.6",
"@kitware/vtk.js": "30.4.1",
"@cornerstonejs/adapters": "^2.2.20",
"@cornerstonejs/core": "^2.2.20",
"@kitware/vtk.js": "32.1.1",
"react-color": "^2.19.3"
}
}
@@ -97,7 +97,7 @@ function _getDisplaySetsFromSeries(
};
// Does not get the referenced volumeId during parametric displaySet creation because the
// referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`).
// referenced displaySet is still not available (getDisplaySetByUID returns `undefined`).
displaySet.getReferencedVolumeId = () => {
if (displaySet.referencedVolumeId) {
return displaySet.referencedVolumeId;
@@ -119,6 +119,22 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
const getRangeFromPixelData = (pixelData: Float32Array) => {
let lowest = pixelData[0];
let highest = pixelData[0];
for (let i = 1; i < pixelData.length; i++) {
if (pixelData[i] < lowest) {
lowest = pixelData[i];
}
if (pixelData[i] > highest) {
highest = pixelData[i];
}
}
return [lowest, highest];
};
async function _load(
displaySet,
servicesManager: AppTypes.ServicesManager,
@@ -132,11 +148,11 @@ async function _load(
return volumeLoadObject.promise;
}
displaySet.loadStatus.loading = true;
displaySet.loading = true;
displaySet.isLoaded = false;
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
const promise = _loadParametricMap({
extensionManager,
displaySet,
@@ -149,18 +165,26 @@ async function _load(
promise
.then(() => {
displaySet.loadStatus.loading = false;
displaySet.loadStatus.loaded = true;
displaySet.loading = false;
displaySet.isLoaded = true;
// Broadcast that loading is complete
servicesManager.services.segmentationService._broadcastEvent(
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
{
pmapDisplaySet: displaySet,
}
);
})
.catch(err => {
displaySet.loadStatus.loading = false;
displaySet.loading = false;
displaySet.isLoaded = false;
throw err;
});
return promise;
}
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
const referencedVolumeId = displaySet.getReferencedVolumeId();
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
@@ -189,9 +213,12 @@ async function _loadParametricMap({ extensionManager, displaySet, headers }: wit
},
});
derivedVolume.getScalarData().set(pixelData);
const range = derivedVolume.imageData.getPointData().getScalars().getRange();
const newPixelData = new TypedArrayConstructor(pixelData.length);
for (let i = 0; i < pixelData.length; i++) {
newPixelData[i] = pixelData[i] * 100;
}
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
const range = getRangeFromPixelData(newPixelData);
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
derivedVolume.metadata.voiLut = [windowLevel];
@@ -1,6 +0,0 @@
function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
export default createPMAPToolGroupAndAddTools;
@@ -1,16 +1,19 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef } from 'react';
import { useViewportGrid } from '@ohif/ui';
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } =
props;
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, toolGroupService, customizationService } = servicesManager.services;
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`;
const { displaySetService, segmentationService, uiNotificationService } =
servicesManager.services;
// PMAP viewport will always have a single display set
if (displaySets.length !== 1) {
@@ -32,6 +35,24 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
metadata: referencedDisplaySetMetadata,
};
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
// Add effect to listen for loading complete
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
setPmapIsLoading(false);
}
}
);
return () => {
unsubscribe();
};
}, [pmapDisplaySet]);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
@@ -46,12 +67,25 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
Object.assign(pmapDisplaySetOptions.options, {
colormap: {
name: 'rainbow',
name: 'rainbow_2',
opacity: [
{ value: 0, opacity: 0.5 },
{ value: 1, opacity: 1 },
{ value: 0, opacity: 0 },
{ value: 0.25, opacity: 0.25 },
{ value: 0.5, opacity: 0.5 },
{ value: 0.75, opacity: 0.75 },
{ value: 0.9, opacity: 0.99 },
],
},
voi: {
windowCenter: 50,
windowWidth: 100,
},
});
uiNotificationService.show({
title: 'Parametric Map',
type: 'warning',
message: 'The values are multiplied by 100 in the viewport for better visibility',
});
return (
@@ -61,7 +95,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
displaySets={[referencedDisplaySet, pmapDisplaySet]}
viewportOptions={{
viewportType: 'volume',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
@@ -73,7 +106,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
displaySetOptions,
props,
pmapDisplaySet,
toolGroupId,
viewportOptions.orientation,
viewportOptions.viewportId,
]);
@@ -98,23 +130,33 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
};
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
useEffect(() => {
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
let childrenWithProps = null;
if (toolGroup) {
return;
}
// This creates a custom tool group which has the lifetime of this view only
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
return () => toolGroupService.destroyToolGroup(toolGroupId);
}, [customizationService, toolGroupId, toolGroupService]);
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportId,
key: index,
})
);
});
}
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{pmapIsLoading && (
<LoadingIndicatorTotalPercent
className="h-full w-full"
totalNumbers={null}
percentComplete={null}
loadingText="Loading Parametric Map..."
/>
)}
{getCornerstoneViewport()}
{childrenWithProps}
</div>
</>
);
@@ -3,6 +3,286 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.79",
"@ohif/extension-cornerstone": "3.9.0-beta.79",
"@ohif/extension-default": "3.9.0-beta.79",
"@ohif/i18n": "3.9.0-beta.79",
"@ohif/core": "3.9.2",
"@ohif/extension-cornerstone": "3.9.2",
"@ohif/extension-default": "3.9.2",
"@ohif/i18n": "3.9.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -0,0 +1,55 @@
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const services = servicesManager.services;
const { displaySetService, viewportGridService } = services;
const actions = {
hydrateRTSDisplaySet: ({ displaySet, viewportId }) => {
if (displaySet.Modality !== 'RTSTRUCT') {
throw new Error('Display set is not an RTSTRUCT');
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: displaySet,
type: SegmentationRepresentations.Contour,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
},
};
const definitions = {
hydrateRTSDisplaySet: {
commandFn: actions.hydrateRTSDisplaySet,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'cornerstone-dicom-rt',
};
};
export default commandsModule;
@@ -72,22 +72,27 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
// Instead of throwing error, subscribe to display sets added
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the RT');
}
} else {
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet;
};
}
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
@@ -111,19 +116,13 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct(
extensionManager,
rtDisplaySet,
rtDisplaySet.getReferenceDisplaySet(),
headers
);
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
rtDisplaySet.structureSet = structureSet;
}
const suppressEvents = true;
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
@@ -2,6 +2,7 @@ import { id } from './id';
import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getCommandsModule from './getCommandsModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
@@ -24,6 +25,7 @@ const extension: Types.Extensions.Extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
getCommandsModule,
/**
* PanelModule should provide a list of panels that will be available in OHIF
@@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
});
}
export default async function loadRTStruct(
extensionManager,
rtStructDisplaySet,
referencedDisplaySet,
headers
) {
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
@@ -102,8 +97,6 @@ export default async function loadRTStruct(
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs =
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
// Set here is loading is asynchronous.
// If this function throws its set back to false.
@@ -149,8 +142,7 @@ export default async function loadRTStruct(
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
ContourSequenceArray[c];
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
let isSupported = false;
@@ -194,24 +186,6 @@ export default async function loadRTStruct(
return structureSet;
}
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
imageIdSopInstanceUidPairsEntry =>
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
return referencedDisplaySet.images.map(image => {
return {
imageId: image.imageId,
sopInstanceUID: image.SOPInstanceUID,
};
});
}
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
@@ -10,7 +10,6 @@ function promptHydrateRT({
servicesManager,
rtDisplaySet,
viewportId,
toolGroupId = 'default',
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
@@ -27,16 +26,13 @@ function promptHydrateRT({
callback();
});
window.setTimeout(async () => {
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
toolGroupId,
servicesManager,
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
}, 0);
resolve(isHydrated);
}
});
}
@@ -4,7 +4,9 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
@@ -62,7 +64,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySetRef.current = {
@@ -91,12 +96,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}, [viewportGrid]);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [rtDisplaySet],
viewportId,
});
};
const hydrateRTDisplaySet = useCallback(
({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('hydrateRTSDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
[commandsManager]
);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
@@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
[selectedSegment, segmentationService]
);
useEffect(() => {
@@ -243,7 +251,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
segmentationService.removeSegmentationRepresentations(viewportId);
toolGroupService.destroyToolGroup(toolGroupId);
};
@@ -254,11 +262,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -282,24 +306,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -318,7 +326,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
@@ -3,6 +3,322 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **modes:** don't attempt to retrieve a stage index if HPs ([#4552](https://github.com/OHIF/Viewers/issues/4552)) ([e5286d9](https://github.com/OHIF/Viewers/commit/e5286d9fc2449da50b738e7e6208d489cce1633e))
* **multiframe:** metadata handling of NM studies and loading order ([#4540](https://github.com/OHIF/Viewers/issues/4540)) ([71f06dc](https://github.com/OHIF/Viewers/commit/71f06dcd3e6d24f1e3d931f7db756c26038927da))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
### Bug Fixes
* video playback ([#4497](https://github.com/OHIF/Viewers/issues/4497)) ([610faa5](https://github.com/OHIF/Viewers/commit/610faa5a2738da5eabc40e57e338c359f481e852))
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
### Bug Fixes
* **ui:** display error in ui while loading seg ([#4433](https://github.com/OHIF/Viewers/issues/4433)) ([2e96371](https://github.com/OHIF/Viewers/commit/2e96371b0631a9e5d411b0142300708ab8ba7d27))
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
### Features
* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f))
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Bug Fixes
* **createReport:** early return on cancel in prompt ([#4243](https://github.com/OHIF/Viewers/issues/4243)) ([2ec4692](https://github.com/OHIF/Viewers/commit/2ec4692eaf2349e21b141a2c0b5b104ee10f7a28))
### Features
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
### Features
* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58))
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.79",
"@ohif/extension-cornerstone": "3.9.0-beta.79",
"@ohif/extension-default": "3.9.0-beta.79",
"@ohif/i18n": "3.9.0-beta.79",
"@ohif/core": "3.9.2",
"@ohif/extension-cornerstone": "3.9.2",
"@ohif/extension-default": "3.9.2",
"@ohif/i18n": "3.9.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.81.6",
"@cornerstonejs/core": "^1.81.6",
"@kitware/vtk.js": "30.4.1",
"@cornerstonejs/adapters": "^2.2.20",
"@cornerstonejs/core": "^2.2.20",
"@kitware/vtk.js": "32.1.1",
"react-color": "^2.19.3"
}
}
@@ -14,18 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
const {
Cornerstone3D: {
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
Segmentation: { generateSegmentation },
},
} = adaptersSEG;
@@ -42,87 +46,14 @@ const commandsModule = ({
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
uiNotificationService,
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
cornerstoneViewportService,
} = servicesManager.services;
} = servicesManager.services as AppTypes.Services;
const actions = {
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @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.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
getUpdatedViewportsForSegmentation,
/**
* Creates an empty segmentation for a specified viewport.
* It first checks if the display set associated with the viewport is reconstructable.
* If not, it raises a notification error. Otherwise, it creates a new segmentation
* for the display set after handling the necessary steps for making the viewport
* a volume viewport first
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the target viewport ID.
*
*/
createEmptySegmentationForViewport: async ({ viewportId }) => {
const viewport = getTargetViewport({ viewportId, viewportGridService });
// Todo: add support for multiple display sets
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet.isReconstructable) {
uiNotificationService.show({
title: 'Segmentation',
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
type: 'error',
});
return;
}
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
displaySet,
loadFn: async () => {
const currentSegmentations = segmentationService.getSegmentations();
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
displaySetInstanceUID,
{ label: `Segmentation ${currentSegmentations.length + 1}` }
);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
// Add only one segment for now
segmentationService.addSegment(segmentationId, {
toolGroupId,
segmentIndex: 1,
properties: {
label: 'Segment 1',
},
});
return segmentationId;
},
});
},
/**
* Loads segmentations for a specified viewport.
* The function prepares the viewport for rendering, then loads the segmentation details.
@@ -134,106 +65,30 @@ const commandsModule = ({
*
*/
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
// Todo: handle adding more than one segmentation
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const segmentation = segmentations[0];
const segmentationId = segmentation.id;
const label = segmentation.label;
const segments = segmentation.segments;
delete segmentation.segments;
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
segmentationId,
label,
});
if (segmentation.scalarData) {
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
labelmapVolume.scalarData.set(segmentation.scalarData);
}
segmentationService.addOrUpdateSegmentation(segmentation);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
segments.forEach(segment => {
if (segment === null) {
return;
}
segmentationService.addSegment(segmentationId, {
segmentIndex: segment.segmentIndex,
toolGroupId,
properties: {
color: segment.color,
label: segment.label,
opacity: segment.opacity,
isLocked: segment.isLocked,
visibility: segment.isVisible,
active: segmentation.activeSegmentIndex === segment.segmentIndex,
},
});
});
if (segmentation.centroidsIJK) {
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
}
return segmentationId;
},
});
},
/**
* Loads segmentation display sets for a specified viewport.
* Depending on the modality of the display set (SEG or RTSTRUCT),
* it chooses the appropriate service function to create
* the segmentation for the display set.
* The function then prepares the viewport for rendering segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
* @param params.displaySets - Array of display sets to be loaded for segmentation.
*
*/
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const initialSliceIndex = viewport.getSliceIndex();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
displaySet,
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const segmentation = segmentations[0];
const segmentationId = segmentation.segmentationId;
const label = segmentation.config.label;
const segments = segmentation.config.segments;
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
const segmentation = segmentationService.getSegmentation(segmentationId);
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
return segmentationId;
},
initialSliceIndex,
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
await segmentationService.createLabelmapForDisplaySet(displaySet, {
segmentationId,
segments,
label,
});
segmentationService.addOrUpdateSegmentation(segmentation);
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
segmentationId,
});
return segmentationId;
},
/**
* Generates a segmentation from a given segmentation ID.
@@ -251,25 +106,61 @@ const commandsModule = ({
generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
const { imageIds } = segmentation.representationData.Labelmap;
const segmentationVolume = cache.getVolume(segmentationId);
const referencedVolume = cache.getVolume(referencedVolumeId);
const referencedImages = referencedVolume.getCornerstoneImages();
const segImages = imageIds.map(imageId => cache.getImage(imageId));
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
const labelmaps2D = [];
// Generate fake metadata as an example
labelmapObj.metadata = [];
let z = 0;
for (const segImage of segImages) {
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;
// Use a single pass through the pixel data
for (let i = 0; i < pixelData.length; i++) {
const segment = pixelData[i];
if (segment !== 0) {
segmentsOnLabelmap.add(segment);
}
}
labelmaps2D[z++] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
}
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
const labelmap3D = {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata: [],
labelmaps2D,
};
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
segmentationInOHIF.segments.forEach(segment => {
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
// segmentation service already has a color for each segment
if (!segment) {
return;
}
const segmentIndex = segment.segmentIndex;
const { label, color } = segment;
const { label } = segment;
const firstRepresentation = representations[0];
const color = segmentationService.getSegmentColor(
firstRepresentation.viewportId,
segmentationId,
segment.segmentIndex
);
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255)
@@ -292,12 +183,12 @@ const commandsModule = ({
CodeMeaning: 'Tissue',
},
};
labelmapObj.metadata[segmentIndex] = segmentMetadata;
labelmap3D.metadata[segmentIndex] = segmentMetadata;
});
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmapObj,
labelmap3D,
metaData,
options
);
@@ -339,7 +230,7 @@ const commandsModule = ({
extensionManager,
});
if (promptResult.action !== 1 && promptResult.value) {
if (promptResult.action !== 1 && !promptResult.value) {
return;
}
@@ -445,18 +336,19 @@ const commandsModule = ({
};
const definitions = {
getUpdatedViewportsForSegmentation: {
commandFn: actions.getUpdatedViewportsForSegmentation,
},
/**
* Obsolete?
*/
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
/**
* Obsolete?
*/
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: {
commandFn: actions.generateSegmentation,
},
@@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
@@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: { allowUnmatchedView: true },
viewportOptions: {
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
id: 'segDisplaySetId',
@@ -1,80 +0,0 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
const getPanelModule = ({
commandsManager,
servicesManager,
extensionManager,
configuration,
title,
}: withAppTypes) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
);
};
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
</>
);
};
return [
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
];
};
export default getPanelModule;
@@ -1,6 +1,6 @@
import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
const referencedDisplaySet = referencedDisplaySets[0];
if (!referencedDisplaySet) {
// subscribe to display sets added which means at some point it will be available
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
// here we can also do a little bit of search, since sometimes DICOM SEG
// does not contain the referenced display set uid , and we can just
// see which of the display sets added is more similar and assign it
// to the referencedDisplaySet
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the SEG');
}
const referencedDisplaySet = referencedDisplaySets[0];
} else {
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
displaySet.referencedVolumeId = referencedVolumeId;
return referencedDisplaySet;
};
}
displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
@@ -111,7 +115,7 @@ function _load(
if (
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] &&
_segmentationExists(segDisplaySet, segmentationService)
_segmentationExists(segDisplaySet)
) {
return loadPromises[SOPInstanceUID];
}
@@ -122,17 +126,21 @@ function _load(
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
});
try {
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
});
} catch (e) {
segDisplaySet.loading = false;
return reject(e);
}
}
const suppressEvents = true;
segmentationService
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
.createSegmentationForSEGDisplaySet(segDisplaySet)
.then(() => {
segDisplaySet.loading = false;
resolve();
@@ -161,20 +169,20 @@ async function _loadSegments({
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
segDisplaySet.referencedDisplaySetInstanceUID
);
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
if (!referencedDisplaySet) {
throw new Error('referencedDisplaySet is missing for SEG');
}
const { imageIds } = cachedReferencedVolume;
const { instances: images } = referencedDisplaySet;
const imageIds = images.map(({ imageId }) => imageId);
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
@@ -226,9 +234,8 @@ async function _loadSegments({
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
function _segmentationExists(segDisplaySet) {
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
@@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
// forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentations();
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
if (!segmentations?.length) {
return {
disabled: true,
@@ -3,7 +3,6 @@ import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule';
@@ -28,13 +27,6 @@ const extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* 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,
getCommandsModule,
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
@@ -1,386 +0,0 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
import { SegmentationPanelMode } from '../types/segmentation';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
const components = {
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
};
export default function PanelSegmentation({
servicesManager,
commandsManager,
extensionManager,
configuration,
}: 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()
);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
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);
setSegmentationConfiguration(segmentationService.getConfiguration());
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
// 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);
return toolGroupIds;
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport', {
viewportId: viewportGridService.getActiveViewportId(),
});
};
const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
const onSegmentationDelete = (segmentationId: string) => {
segmentationService.remove(segmentationId);
};
const onSegmentAdd = segmentationId => {
segmentationService.addSegment(segmentationId);
};
const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegment(segmentationId, segmentIndex);
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
// const toolGroupId =
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
});
};
const onSegmentEdit = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
});
};
const onSegmentationEdit = segmentationId => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const { label } = segmentation;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.addOrUpdateSegmentation(
{
id: segmentationId,
label,
},
false, // suppress event
true // notYetUpdatedAtSource
);
});
};
const onSegmentColorClick = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { color, opacity } = segment;
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: opacity / 255.0,
};
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
newRgbaColor.r,
newRgbaColor.g,
newRgbaColor.b,
newRgbaColor.a * 255.0,
]);
});
};
const onSegmentDelete = (segmentationId, segmentIndex) => {
segmentationService.removeSegment(segmentationId, segmentIndex);
};
// segment hide
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex];
const isVisible = !segmentInfo.isVisible;
const toolGroupIds = getToolGroupIds(segmentationId);
// Todo: right now we apply the visibility to all tool groups
toolGroupIds.forEach(toolGroupId => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
};
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
};
const onToggleSegmentationVisibility = segmentationId => {
segmentationService.toggleSegmentationVisibility(segmentationId);
const segmentation = segmentationService.getSegmentation(segmentationId);
const isVisible = segmentation.isVisible;
const segments = segmentation.segments;
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
segments.forEach((segment, segmentIndex) => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
});
};
const _setSegmentationConfiguration = useCallback(
(segmentationId, key, value) => {
segmentationService.setConfiguration({
segmentationId,
[key]: value,
});
},
[segmentationService]
);
const onSegmentationDownload = segmentationId => {
commandsManager.runCommand('downloadSegmentation', {
segmentationId,
});
};
const storeSegmentation = async segmentationId => {
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
// Show the exported report in the active viewport as read only (similar to SR)
if (displaySetInstanceUIDs) {
// clear the segmentation that we exported, similar to the storeMeasurement
// where we remove the measurements and prompt again the user if they would like
// to re-read the measurements in a SR read only viewport
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
};
const onSegmentationDownloadRTSS = segmentationId => {
commandsManager.runCommand('downloadRTSS', {
segmentationId,
});
};
const SegmentationGroupTableComponent =
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
const allowAddSegment = configuration?.addSegment;
const onSegmentationAddWrapper =
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
? configuration?.onSegmentationAdd
: onSegmentationAdd;
return (
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
);
}
PanelSegmentation.propTypes = {
commandsManager: PropTypes.shape({
runCommand: PropTypes.func.isRequired,
}),
servicesManager: PropTypes.shape({
services: PropTypes.shape({
segmentationService: PropTypes.shape({
getSegmentation: PropTypes.func.isRequired,
getSegmentations: PropTypes.func.isRequired,
toggleSegmentationVisibility: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
@@ -1,62 +0,0 @@
import React from 'react';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment',
value: { label },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
return (
<Input
label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus
className="border-primary-main bg-black"
type="text"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
},
},
});
}
}
export default callInputDialog;
@@ -1,33 +0,0 @@
// Todo: use defaults in cs3d
const initialState = {
renderOutline: true,
renderFill: true,
outlineOpacity: 0.9,
outlineWidth: 3,
fillOpacity: 0.9,
fillOpacityInactive: 0.8,
renderInactiveSegmentations: true,
};
const reducer = (state, action) => {
switch (action.type) {
case 'RENDER_OUTLINE':
return { ...state, renderOutline: action.payload.value };
case 'RENDER_FILL':
return { ...state, renderFill: action.payload.value };
case 'SET_OUTLINE_OPACITY':
return { ...state, outlineOpacity: action.payload.value };
case 'SET_OUTLINE_WIDTH':
return { ...state, outlineWidth: action.payload.value };
case 'SET_FILL_OPACITY':
return { ...state, fillOpacity: action.payload.value };
case 'SET_FILL_OPACITY_INACTIVE':
return { ...state, fillOpacityInactive: action.payload.value };
case 'RENDER_INACTIVE_SEGMENTATIONS':
return { ...state, renderInactiveSegmentations: action.payload.value };
default:
return state;
}
};
export { initialState, reducer };
@@ -1,203 +0,0 @@
import { Enums, cache } from '@cornerstonejs/core';
/**
* Updates the viewports in preparation for rendering segmentations.
* Evaluates each viewport to determine which need modifications,
* then for those viewports, changes them to a volume type and ensures
* they are ready for segmentation rendering.
*
* @param {Object} params - Parameters for the function.
* @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.displaySet - the display set.
* @param params.initialSliceIndex - The initial slice index.
*
* @returns Returns true upon successful update of viewports for segmentation rendering.
*/
async function updateViewportsForSegmentationRendering({
viewportId,
loadFn,
servicesManager,
displaySet,
initialSliceIndex = null,
}: {
viewportId: string;
loadFn: () => Promise<string>;
servicesManager: AppTypes.ServicesManager;
displaySet?: any;
initialSliceIndex?: number;
}) {
const { cornerstoneViewportService, segmentationService, viewportGridService } =
servicesManager.services;
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const referencedDisplaySetInstanceUID =
displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
const updatedViewports = getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
displaySet,
});
// create Segmentation callback which needs to be waited until
// the volume is created (if coming from stack)
const createSegmentationForVolume = async () => {
const segmentationId = await loadFn();
segmentationService.hydrateSegmentation(segmentationId);
};
// the reference volume that is used to draw the segmentation. so check if the
// volume exists in the cache (the target Viewport is already a volume viewport)
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
volumeId.includes(referencedDisplaySetInstanceUID)
);
updatedViewports.forEach(async viewport => {
viewport.viewportOptions = {
...viewport.viewportOptions,
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
needsRerendering: true,
};
const viewportId = viewport.viewportId;
// maintain the prehydration slice on the target viewport only
if (viewportId === targetViewportId) {
viewport.viewportOptions.initialImageOptions = {
index: initialSliceIndex,
useOnce: true,
};
}
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const prevCamera = csViewport.getCamera();
// only run the createSegmentationForVolume for the targetViewportId
// since the rest will get handled by cornerstoneViewportService
if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) {
await createSegmentationForVolume();
return;
}
const createNewSegmentationWhenVolumeMounts = async evt => {
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
ac.uid.includes(referencedDisplaySetInstanceUID)
);
// Note: make sure to re-grab the viewport since it might have changed
// during the time it took for the volume to be mounted, for instance
// the stack viewport has been changed to a volume viewport
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
volumeViewport.setCamera(prevCamera);
volumeViewport.element.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
if (!isTheActiveViewportVolumeMounted) {
// it means it is one of those other updated viewports so just update the camera
return;
}
if (viewportId === targetViewportId) {
await createSegmentationForVolume();
}
};
csViewport.element.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
});
// Set the displaySets for the viewports that require to be updated
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @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.displaySet - the display set.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
function getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
displaySet,
}: withAppTypes) {
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
servicesManager.services;
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
const referenceDisplaySetInstanceUID =
displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
referenceDisplaySetInstanceUID,
isHangingProtocolLayout
);
viewports.forEach((viewport, viewportId) => {
if (
targetViewportId === viewportId ||
updatedViewports.find(v => v.viewportId === viewportId)
) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segmentationFrameOfReferenceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
needsRerendering: true,
},
});
}
});
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
};
@@ -11,7 +11,7 @@ function promptHydrateSEG({
segDisplaySet,
viewportId,
preHydrateCallbacks,
hydrateSEGDisplaySet,
hydrateCallback,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
@@ -28,7 +28,7 @@ function promptHydrateSEG({
});
window.setTimeout(async () => {
const isHydrated = await hydrateSEGDisplaySet({
const isHydrated = await hydrateCallback({
segDisplaySet,
viewportId,
});
@@ -1,10 +1,10 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
@@ -37,7 +37,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}
const segDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
@@ -61,7 +60,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet,
segDisplaySet
@@ -98,18 +101,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
displaySets={[referencedDisplaySet, segDisplaySet]}
displaySets={[segDisplaySet]}
viewportOptions={{
viewportType: 'volume',
viewportType: viewportOptions.viewportType,
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
@@ -139,12 +141,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
);
const hydrateSEG = useCallback(() => {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
useEffect(() => {
if (segIsLoading) {
return;
@@ -155,15 +180,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateSEGDisplaySet,
hydrateCallback: hydrateSEG,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
segmentationService.clearSegmentationRepresentations(viewportId);
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
@@ -223,39 +251,31 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
return;
}
// keep the already stored segmentationPresentation for this viewport in memory
// so that we can restore it after hydrating the SEG
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// always start fresh for this viewport since it is special type of viewport
// that should only show one segmentation at a time.
segmentationService.clearSegmentationRepresentations(viewportId);
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
// e.g., another seg displayset is dragged into the viewport
segmentationService.clearSegmentationRepresentations(viewportId);
// Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
useEffect(() => {
setIsHydrated(segDisplaySet.isHydrated);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [segDisplaySet]);
const hydrateSEGDisplaySet = useCallback(
({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
},
[commandsManager]
);
const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
@@ -264,16 +284,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
hydrateSEG();
}, [storePresentationState, hydrateSEG]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -292,7 +307,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
@@ -332,18 +347,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
@@ -362,13 +365,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
);
}
OHIFCornerstoneSEGViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
@@ -3,6 +3,315 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **multiframe:** metadata handling of NM studies and loading order ([#4540](https://github.com/OHIF/Viewers/issues/4540)) ([71f06dc](https://github.com/OHIF/Viewers/commit/71f06dcd3e6d24f1e3d931f7db756c26038927da))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
### Bug Fixes
* video playback ([#4497](https://github.com/OHIF/Viewers/issues/4497)) ([610faa5](https://github.com/OHIF/Viewers/commit/610faa5a2738da5eabc40e57e338c359f481e852))
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
### Features
* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f))
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Bug Fixes
* **sr:** load existing point, if there is 2nd point in renderableData (Fix rotation in arrow annotation) ([#4356](https://github.com/OHIF/Viewers/issues/4356)) ([7353f7f](https://github.com/OHIF/Viewers/commit/7353f7f069446f8484278c2cff5b09149cfa23eb))
### Features
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"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.9.0-beta.79",
"@ohif/extension-cornerstone": "3.9.0-beta.79",
"@ohif/extension-measurement-tracking": "3.9.0-beta.79",
"@ohif/ui": "3.9.0-beta.79",
"@ohif/core": "3.9.2",
"@ohif/extension-cornerstone": "3.9.2",
"@ohif/extension-measurement-tracking": "3.9.2",
"@ohif/ui": "3.9.2",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.82.4",
"@cornerstonejs/core": "^1.82.4",
"@cornerstonejs/tools": "^1.82.4",
"@cornerstonejs/adapters": "^2.2.20",
"@cornerstonejs/core": "^2.2.20",
"@cornerstonejs/tools": "^2.2.20",
"classnames": "^2.3.2"
}
}
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import CodeNameCodeSequenceValues from '../constants/CodeNameCodeSequenceValues';
import { CodeNameCodeSequenceValues } from '../enums';
import formatContentItemValue from '../utils/formatContentItem';
const EMPTY_TAG_VALUE = '[empty]';
@@ -15,8 +15,15 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const {
commandsManager,
children,
dataSource,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const [appConfig] = useAppConfig();
@@ -63,6 +70,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1];
}
if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null;
@@ -72,6 +80,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
@@ -84,6 +93,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
};
}
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
@@ -135,6 +149,10 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
newMeasurementSelected,
displaySetService
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
if (!referencedDisplaySet || !referencedDisplaySetMetadata) {
return;
}
setMeasurementSelected(newMeasurementSelected);
setActiveImageDisplaySetData(referencedDisplaySet);
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
@@ -151,6 +169,10 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
// new measurement
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
const imageIds = csViewport.getImageIds();
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
@@ -289,17 +311,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -395,6 +412,10 @@ async function _getViewportReferencedDisplaySetData(
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
}
if (!displaySetInstanceUID) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const image0 = referencedDisplaySet.images[0];
@@ -1,18 +0,0 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
};
export { CodeNameCodeSequenceValues as default };
@@ -1,7 +0,0 @@
export default {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};
@@ -0,0 +1,44 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
export const SCOORDTypes = {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};
export const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
FindingSiteSCT: '363698007', // SCT
};
export const CodingSchemeDesignators = {
SRT: 'SRT',
SCT: 'SCT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
export const RelationshipType = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
};
const enums = {
CodeNameCodeSequenceValues,
CodingSchemeDesignators,
RelationshipType,
SCOORDTypes,
};
export default enums;
@@ -1,32 +1,37 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import { adaptersSR } from '@cornerstonejs/adapters';
import CodeNameCodeSequenceValues from './constants/CodeNameCodeSequenceValues';
import addSRAnnotation from './utils/addSRAnnotation';
import isRehydratable from './utils/isRehydratable';
import {
SOPClassHandlerName,
SOPClassHandlerId,
SOPClassHandlerId3D,
SOPClassHandlerName3D,
} from './id';
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
type InstanceMetadata = Types.InstanceMetadata;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
// TODO ->
// Add SR thumbnail
// Make viewport
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
/**
* TODO
* - [ ] Add SR thumbnail
* - [ ] Make viewport
* - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport
*/
const sopClassUids = [
'1.2.840.10008.5.1.4.1.1.88.11', // BASIC TEXT SR
'1.2.840.10008.5.1.4.1.1.88.22', // ENHANCED SR
'1.2.840.10008.5.1.4.1.1.88.33', // COMPREHENSIVE SR
'1.2.840.10008.5.1.4.1.1.88.34', // Comprehensive 3D SR
// '1.2.840.10008.5.1.4.1.1.88.50', // Mammography CAD SR
sopClassDictionary.BasicTextSR,
sopClassDictionary.EnhancedSR,
sopClassDictionary.ComprehensiveSR,
];
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const validateSameStudyUID = (uid: string, instances): void => {
instances.forEach(it => {
if (it.StudyInstanceUID !== uid) {
@@ -36,18 +41,6 @@ const validateSameStudyUID = (uid: string, instances): void => {
});
};
const CodingSchemeDesignators = {
SRT: 'SRT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
};
const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
};
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* Adds instances to the DICOM SR series, rather than creating a new
* series, so that as SR's are saved, they append to the series, and the
@@ -102,6 +95,8 @@ function _getDisplaySetsFromSeries(
} = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);
const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR;
const isImagingMeasurementReport =
ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
@@ -114,7 +109,7 @@ function _getDisplaySetsFromSeries(
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId,
SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId,
SOPClassUID,
instances,
referencedImages: null,
@@ -132,11 +127,21 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
async function _load(displaySet, servicesManager: AppTypes.ServicesManager, extensionManager) {
/**
* Loads the display set with the given services and extension manager.
* @param srDisplaySet - The display set to load.
* @param servicesManager - The services manager containing displaySetService and measurementService.
* @param extensionManager - The extension manager containing data sources.
*/
async function _load(
srDisplaySet: Types.DisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager: AppTypes.ExtensionManager
) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = displaySet.instance;
const { ContentSequence } = srDisplaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
for (const prop in obj) {
@@ -147,9 +152,9 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
} 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,
StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID,
SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID,
});
if (parentObj && key) {
parentObj[key] = new Float32Array(value);
@@ -158,16 +163,16 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
}
}
if (displaySet.isLoaded !== true) {
if (srDisplaySet.isLoaded !== true) {
await retrieveBulkData(ContentSequence);
}
if (displaySet.isImagingMeasurementReport) {
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
if (srDisplaySet.isImagingMeasurementReport) {
srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence);
srDisplaySet.measurements = _getMeasurements(ContentSequence);
} else {
displaySet.referencedImages = [];
displaySet.measurements = [];
srDisplaySet.referencedImages = [];
srDisplaySet.measurements = [];
}
const mappings = measurementService.getSourceMappings(
@@ -175,28 +180,30 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
displaySet.isHydrated = false;
displaySet.isRehydratable = isRehydratable(displaySet, mappings);
displaySet.isLoaded = true;
srDisplaySet.isHydrated = false;
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
srDisplaySet.isLoaded = true;
// Check currently added displaySets and add measurements if the sources exist.
/** Check currently added displaySets and add measurements if the sources exist */
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
srDisplaySet,
activeDisplaySet,
dataSource,
servicesManager
);
});
// Subscribe to new displaySets as the source may come in after.
/** Subscribe to new displaySets as the source may come in after */
displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
const { displaySetsAdded } = data;
// If there are still some measurements that have not yet been loaded into cornerstone,
// See if we can load them onto any of the new displaySets.
/**
* If there are still some measurements that have not yet been loaded into cornerstone,
* See if we can load them onto any of the new displaySets.
*/
displaySetsAdded.forEach(newDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
srDisplaySet,
newDisplaySet,
dataSource,
servicesManager
@@ -205,6 +212,14 @@ async function _load(displaySet, servicesManager: AppTypes.ServicesManager, exte
});
}
/**
* Checks if measurements can be added to a display set.
*
* @param srDisplaySet - The source display set containing measurements.
* @param newDisplaySet - The new display set to check if measurements can be added.
* @param dataSource - The data source used to retrieve image IDs.
* @param servicesManager - The services manager.
*/
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
@@ -212,108 +227,103 @@ function _checkIfCanAddMeasurementsToDisplaySet(
servicesManager: AppTypes.ServicesManager
) {
const { customizationService } = servicesManager.services;
let unloadedMeasurements = srDisplaySet.measurements.filter(
const unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
if (unloadedMeasurements.length === 0) {
// All already loaded!
if (
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
return;
}
if ((!newDisplaySet) instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
// const { sopClassUids } = newDisplaySet;
// Create a Set for faster lookups
// const sopClassUidSet = new Set(sopClassUids);
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIds) {
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
const key = `${SOPInstanceUID}:${frameNumber || 1}`;
imageIdMap.set(key, imageId);
}
if (!unloadedMeasurements?.length) {
return;
}
if (newDisplaySet.unsupported) {
return;
}
const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR;
const { sopClassUids } = newDisplaySet;
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
measurement.coords.some(coord =>
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
)
);
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
if (unloadedMeasurements.length === 0) {
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
return;
}
const SOPInstanceUIDs = [];
unloadedMeasurements.forEach(measurement => {
const { coords } = measurement;
coords.forEach(coord => {
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID);
}
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
for (const imageId of imageIdsForDisplaySet) {
if (!unloadedMeasurements.length) {
// All measurements loaded.
return;
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
// if it is 3d SR we can just add the SR annotation
if (is3DSR) {
addSRAnnotation(measurement, null, null);
measurement.loaded = true;
continue;
}
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence;
if (!referencedSOPSequence) {
continue;
}
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
const { ReferencedSOPInstanceUID } = referencedSOPSequence;
const frame = referencedSOPSequence.ReferencedFrameNumber || 1;
const key = `${ReferencedSOPInstanceUID}:${frame}`;
const imageId = imageIdMap.get(key);
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
if (
imageId &&
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
) {
addSRAnnotation(measurement, imageId, frame);
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
const frame =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
// Update measurement properties
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
/** 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);
}
}
unloadedMeasurements.splice(j, 1);
}
}
}
/**
* Checks if a measurement references a specific SOP Instance UID.
* @param measurement - The measurement object.
* @param SOPInstanceUID - The SOP Instance UID to check against.
* @param frameNumber - The frame number to check against (optional).
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
*/
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
const { coords } = measurement;
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
// Standard. But for now, we will support only one ReferenceFrameNumber.
/**
* NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
* Standard. But for now, we will support only one ReferenceFrameNumber.
*/
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
@@ -326,27 +336,46 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
return true;
}
}
return false;
}
/**
* Retrieves the SOP class handler module.
*
* @param {Object} options - The options for retrieving the SOP class handler module.
* @param {Object} options.servicesManager - The services manager.
* @param {Object} options.extensionManager - The extension manager.
* @returns {Array} An array containing the SOP class handler module.
*/
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
return [
{
name: SOPClassHandlerName,
sopClassUids,
getDisplaySetsFromSeries,
},
{
name: SOPClassHandlerName3D,
sopClassUids: [sopClassDictionary.Comprehensive3DSR],
getDisplaySetsFromSeries,
},
];
}
/**
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
* @returns {Array} - The array of measurements.
*/
function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
@@ -363,7 +392,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
const mergedContentSequencesByTrackingUniqueIdentifiers =
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
@@ -372,7 +400,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
const measurement = _processMeasurement(mergedContentSequence);
if (measurement) {
measurements.push(measurement);
}
@@ -382,6 +409,12 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
return measurements;
}
/**
* Retrieves merged content sequences by tracking unique identifiers.
*
* @param {Array} MeasurementGroups - The measurement groups.
* @returns {Object} - The merged content sequences by tracking unique identifiers.
*/
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
@@ -393,7 +426,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
);
if (!TrackingUniqueIdentifierItem) {
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
}
@@ -422,6 +454,15 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
return mergedContentSequencesByTrackingUniqueIdentifiers;
}
/**
* Processes the measurement based on the merged content sequence.
* If the merged content sequence contains SCOORD or SCOORD3D value types,
* it calls the _processTID1410Measurement function.
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
*
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
* @returns {any} - The processed measurement result.
*/
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
@@ -434,11 +475,21 @@ function _processMeasurement(mergedContentSequence) {
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
}
/**
* Processes TID 1410 style measurements from the mergedContentSequence.
* TID 1410 style measurements have a SCOORD or SCOORD3D at the top level,
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
*
* @param mergedContentSequence - The merged content sequence containing the measurements.
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
*/
function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
const graphicItem = mergedContentSequence.find(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
);
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
@@ -465,7 +516,6 @@ function _processTID1410Measurement(mergedContentSequence) {
NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
@@ -473,12 +523,29 @@ function _processTID1410Measurement(mergedContentSequence) {
}
});
const findingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT &&
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT
);
if (findingSites.length) {
measurement.labels.push({
label: CodeNameCodeSequenceValues.FindingSiteSCT,
value: findingSites[0].ConceptCodeSequence.CodeMeaning,
});
}
return measurement;
}
/**
* Processes the non-geometrically defined measurement from the merged content sequence.
*
* @param mergedContentSequence The merged content sequence containing the measurement data.
* @returns The processed measurement object.
*/
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
const TrackingIdentifierContentItem = mergedContentSequence.find(
@@ -508,10 +575,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
value: finding.ConceptCodeSequence.CodeMeaning,
});
}
@@ -523,12 +590,13 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
FindingSite.ConceptCodeSequence.CodeValue ===
Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
);
if (cornerstoneFreeTextFindingSite) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
});
}
@@ -538,15 +606,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') {
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
return;
}
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
if (coords) {
measurement.coords.push(coords);
}
@@ -561,51 +626,47 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
return measurement;
}
function _getCoordsFromSCOORDOrSCOORD3D(item) {
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
if (
!(
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
)
) {
console.warn(
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"`
);
return;
}
/**
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
* @param {object} graphicItem - The graphic item containing the coordinates.
* @returns {object} - The extracted coordinates.
*/
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
const { ValueType, GraphicType, GraphicData } = graphicItem;
const coords = { ValueType, GraphicType, GraphicData };
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
if (ValueType === 'SCOORD') {
const { ReferencedSOPSequence } = item.ContentSequence;
coords.ReferencedSOPSequence = ReferencedSOPSequence;
} else if (ValueType === 'SCOORD3D') {
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
}
coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence;
coords.ReferencedFrameOfReferenceSequence =
graphicItem.ReferencedFrameOfReferenceUID ||
graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence;
return coords;
}
};
/**
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
* @returns {Object} - An object containing the label and value.
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
* Example: { label: 'Long Axis', value: '31.00 mm' }
*/
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
return {
label: CodeMeaning,
value: `${formatedNumericValue} ${CodeValue}`,
}; // E.g. Long Axis: 31.0 mm
}
/**
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
* @returns {Array} - The list of referenced images.
*/
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
@@ -618,6 +679,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
);
if (!ImageLibraryGroup) {
return [];
}
const referencedImages = [];
@@ -641,6 +705,15 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
return referencedImages;
}
/**
* Converts a DICOM sequence to an array.
* If the sequence is null or undefined, an empty array is returned.
* If the sequence is already an array, it is returned as is.
* Otherwise, the sequence is wrapped in an array and returned.
*
* @param {any} sequence - The DICOM sequence to convert.
* @returns {any[]} - The converted array.
*/
function _getSequenceAsArray(sequence) {
if (!sequence) {
return [];
+4 -1
View File
@@ -5,4 +5,7 @@ const id = packageJson.name;
const SOPClassHandlerName = 'dicom-sr';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
export { SOPClassHandlerName, SOPClassHandlerId, id };
const SOPClassHandlerName3D = 'dicom-sr-3d';
const SOPClassHandlerId3D = `${id}.sopClassHandlerModule.${SOPClassHandlerName3D}`;
export { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerName3D, SOPClassHandlerId3D, id };
@@ -8,6 +8,7 @@ import { id } from './id.js';
import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
import Enums from './enums';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
@@ -70,4 +71,4 @@ const dicomSRExtension = {
export default dicomSRExtension;
// Put static exports here so they can be type checked
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol, Enums, toolNames };
+54 -2
View File
@@ -9,16 +9,28 @@ import {
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
import addToolInstance from './utils/addToolInstance';
import { Types } from '@ohif/core';
import toolNames from './tools/toolNames';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
export default function init({
configuration = {},
servicesManager,
}: Types.Extensions.ExtensionParams): void {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
@@ -28,10 +40,26 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
addToolInstance(toolNames.SRAngle, AngleTool);
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
const csTools3DVer1MeasurementSource = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
const { POINT } = measurementService.VALUE_TYPES;
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SRSCOORD3DPoint',
POINT,
SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement
);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
@@ -49,4 +77,28 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
SRRectangleROI: dashedLine,
global: {},
});
measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
({ viewportId, measurement, isConsumed }) => {
if (isConsumed) {
return;
}
try {
const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { viewPlaneNormal } = currentViewport.getCamera();
const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport(
currentViewport as StackViewport,
measurement.points[0],
viewPlaneNormal
);
const imageIndex = (currentViewport as StackViewport)
.getImageIds()
.indexOf(referencedImageId);
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
} catch (error) {
console.warn('Unable to jump to image based on measurement coordinate', error);
}
}
);
}
@@ -1,11 +1,11 @@
import { SOPClassHandlerId } from './id';
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
export default function onModeEnter({ servicesManager }) {
const { displaySetService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId
ds => ds.SOPClassHandlerId === SOPClassHandlerId || ds.SOPClassHandlerId === SOPClassHandlerId3D
);
srDisplaySets.forEach(ds => {
@@ -7,10 +7,11 @@ import {
Types as cs3DToolsTypes,
} from '@cornerstonejs/tools';
import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule';
import SCOORD_TYPES from '../constants/scoordTypes';
import { SCOORDTypes } from '../enums';
import toolNames from './toolNames';
export default class DICOMSRDisplayTool extends AnnotationTool {
static toolName = 'DICOMSRDisplay';
static toolName = toolNames.DICOMSRDisplay;
constructor(
toolProps = {},
@@ -112,19 +113,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
let canvasCoordinatesAdapter;
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORDTypes.POINT:
renderMethod = this.renderPoint;
break;
case SCOORD_TYPES.MULTIPOINT:
case SCOORDTypes.MULTIPOINT:
renderMethod = this.renderMultipoint;
break;
case SCOORD_TYPES.POLYLINE:
case SCOORDTypes.POLYLINE:
renderMethod = this.renderPolyLine;
break;
case SCOORD_TYPES.CIRCLE:
case SCOORDTypes.CIRCLE:
renderMethod = this.renderEllipse;
break;
case SCOORD_TYPES.ELLIPSE:
case SCOORDTypes.ELLIPSE:
renderMethod = this.renderEllipse;
canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
break;
@@ -229,7 +230,11 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
// This gives us one point for arrow
canvasCoordinates.push(viewport.worldToCanvas(point));
// We get the other point for the arrow by using the image size
if (data[1] !== undefined) {
canvasCoordinates.push(viewport.worldToCanvas(data[1]));
}
else{
// We get the other point for the arrow by using the image size
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
let xOffset = 10;
@@ -248,6 +253,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
]);
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
}
const arrowUID = `${index}`;
@@ -335,7 +343,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
}
const { annotationUID, data = {} } = annotation;
const { label } = data;
const { labels } = data;
const { color } = options;
let adaptedCanvasCoordinates = canvasCoordinates;
@@ -343,7 +351,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
if (typeof canvasCoordinatesAdapter === 'function') {
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
}
const textLines = this._getTextBoxLinesFromLabels(label);
const textLines = this._getTextBoxLinesFromLabels(labels);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
if (!annotation.data?.handles?.textBox?.worldPosition) {
@@ -0,0 +1,203 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
import {
annotation,
drawing,
utilities,
Types as cs3DToolsTypes,
AnnotationDisplayTool,
} from '@cornerstonejs/tools';
import toolNames from './toolNames';
import { Annotation } from '@cornerstonejs/tools/dist/types/types';
export default class SCOORD3DPointTool extends AnnotationDisplayTool {
static toolName = toolNames.SRSCOORD3DPoint;
constructor(
toolProps = {},
defaultToolProps = {
configuration: {},
}
) {
super(toolProps, defaultToolProps);
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 5);
const lines = [];
return lines;
}
// This tool should not inherit from AnnotationTool and we should not need
// to add the following lines.
isPointNearTool = () => null;
getHandleNearImagePoint = () => null;
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
const { viewport } = enabledElement;
const { element } = viewport;
const annotations = annotation.state.getAnnotations(this.getToolName(), element);
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
if (!annotations?.length) {
return;
}
// Filter toolData to only render the data for the active SR.
const filteredAnnotations = annotations;
if (!viewport._actors?.size) {
return;
}
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
toolGroupId: this.toolGroupId,
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
const annotationUID = annotation.annotationUID;
const { renderableData } = annotation.data;
const { POINT: points } = renderableData;
styleSpecifier.annotationUID = annotationUID;
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color = this.getStyle('color', styleSpecifier, annotation);
const options = {
color,
lineDash,
lineWidth,
};
const point = points[0][0];
// check if viewport can render it
const viewable = viewport.isReferenceViewable(
{ FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point },
{ asNearbyProjection: true }
);
if (!viewable) {
continue;
}
// render the point
const arrowPointCanvas = viewport.worldToCanvas(point);
// Todo: configure this
const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20];
const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas];
drawing.drawArrow(
svgDrawingHelper,
annotationUID,
'1',
canvasCoordinates[1],
canvasCoordinates[0],
{
color: options.color,
width: options.lineWidth,
}
);
this.renderTextBox(
svgDrawingHelper,
viewport,
canvasCoordinates,
annotation,
styleSpecifier,
options
);
}
};
renderTextBox(
svgDrawingHelper,
viewport,
canvasCoordinates,
annotation,
styleSpecifier,
options = {}
) {
if (!canvasCoordinates || !annotation) {
return;
}
const { annotationUID, data = {} } = annotation;
const { labels } = data;
const textLines = [];
for (const label of labels) {
// make this generic
// fix this
if (label.label === '363698007') {
textLines.push(`Finding Site: ${label.value}`);
}
}
const { color } = options;
const adaptedCanvasCoordinates = canvasCoordinates;
// adapt coordinates if there is an adapter
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
if (!annotation.data?.handles?.textBox?.worldPosition) {
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
}
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
const textBoxUID = '1';
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
const boundingBox = drawing.drawLinkedTextBox(
svgDrawingHelper,
annotationUID,
textBoxUID,
textLines,
textBoxPosition,
canvasCoordinates,
{},
{
...textBoxOptions,
color,
}
);
const { x: left, y: top, width, height } = boundingBox;
annotation.data.handles.textBox.worldBoundingBox = {
topLeft: viewport.canvasToWorld([left, top]),
topRight: viewport.canvasToWorld([left + width, top]),
bottomLeft: viewport.canvasToWorld([left, top + height]),
bottomRight: viewport.canvasToWorld([left + width, top + height]),
};
}
public getLinkedTextBoxStyle(
specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier,
annotation?: Annotation
): Record<string, unknown> {
// Todo: this function can be used to set different styles for different toolMode
// for the textBox.
return {
visibility: this.getStyle('textBoxVisibility', specifications, annotation),
fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation),
fontSize: this.getStyle('textBoxFontSize', specifications, annotation),
color: this.getStyle('textBoxColor', specifications, annotation),
shadow: this.getStyle('textBoxShadow', specifications, annotation),
background: this.getStyle('textBoxBackground', specifications, annotation),
lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation),
lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation),
};
}
}
@@ -1,7 +1,5 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
const toolNames = {
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
DICOMSRDisplay: 'DICOMSRDisplay',
SRLength: 'SRLength',
SRBidirectional: 'SRBidirectional',
SREllipticalROI: 'SREllipticalROI',
@@ -11,6 +9,7 @@ const toolNames = {
SRCobbAngle: 'SRCobbAngle',
SRRectangleROI: 'SRRectangleROI',
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
SRSCOORD3DPoint: 'SRSCOORD3DPoint',
};
export default toolNames;
@@ -0,0 +1,62 @@
const SRSCOOR3DProbe = {
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Probe tool: Missing metadata or data');
return null;
}
const { toolName } = metadata;
const { points } = data.handles;
const displayText = getDisplayText(annotation);
return {
uid: annotationUID,
points,
metadata,
toolName: metadata.toolName,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType?.(toolName) ?? null,
};
},
};
function getDisplayText(annotation) {
const { data } = annotation;
if (!data) {
return [''];
}
const { labels } = data;
const displayText = [];
for (const label of labels) {
// make this generic
if (label.label === '33636980076') {
displayText.push(`Finding Site: ${label.value}`);
}
}
return displayText;
}
export default SRSCOOR3DProbe;
@@ -0,0 +1,67 @@
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData } from '@cornerstonejs/core';
import getRenderableData from './getRenderableData';
import toolNames from '../tools/toolNames';
export default function addSRAnnotation(measurement, imageId, frameNumber) {
let toolName = toolNames.DICOMSRDisplay;
const renderableData = measurement.coords.reduce((acc, coordProps) => {
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
return acc;
}, {});
const { TrackingUniqueIdentifier } = measurement;
const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0];
const graphicTypePoints = renderableData[graphicType];
/** TODO: Read the tool name from the DICOM SR identification type in the future. */
let frameOfReferenceUID = null;
if (imageId) {
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID;
}
if (valueType === 'SCOORD3D') {
toolName = toolNames.SRSCOORD3DPoint;
// get the ReferencedFrameOfReferenceUID from the measurement
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
}
const SRAnnotation: Types.Annotation = {
annotationUID: TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName,
valueType,
graphicType,
FrameOfReferenceUID: frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels?.[0]?.value || undefined,
displayText: measurement.displayText || undefined,
handles: {
textBox: measurement.textBox ?? {},
points: graphicTypePoints[0],
},
cachedStats: {},
frameNumber,
renderableData,
TrackingUniqueIdentifier,
labels: measurement.labels,
},
};
/**
* const annotationManager = annotation.annotationState.getAnnotationManager();
* was not triggering annotation_added events.
*/
annotation.state.addAnnotation(SRAnnotation);
console.debug('Adding SR annotation:', SRAnnotation);
}
@@ -1,8 +1,14 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(name: string, toolClass, configuration?): void {
export default function addToolInstance(name: string, toolClass, configuration = {}): void {
class InstanceClass extends toolClass {
static toolName = name;
constructor(toolProps, defaultToolProps) {
toolProps.configuration = toolProps.configuration
? { ...toolProps.configuration, ...configuration }
: configuration;
super(toolProps, defaultToolProps);
}
}
addTool(InstanceClass);
}
@@ -61,6 +61,11 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
const imageSet = new ImageSet(instances);
const instance = instances[0];
if (!instance) {
return;
}
imageSet.setAttributes({
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: instance.SeriesDate,
@@ -1,3 +1,7 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
/**
* Extracts the label from the toolData imported from dcmjs. We need to do this
* as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools.
@@ -9,13 +13,15 @@
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
);
if (freeTextLabel) {
return freeTextLabel.CodeMeaning;
}
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) {
return finding.CodeMeaning;
}
}
@@ -1,106 +1,49 @@
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';
import { SCOORDTypes } from '../enums';
const EPSILON = 1e-4;
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
const toolName = toolNames.DICOMSRDisplay;
const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
renderableData: {},
labels: measurement.labels,
imageId,
};
measurement.coords.forEach(coord => {
const { GraphicType, GraphicData } = coord;
if (measurementData.renderableData[GraphicType] === undefined) {
measurementData.renderableData[GraphicType] = [];
const getRenderableCoords = ({ GraphicData, ValueType, imageId }) => {
const renderableData = [];
if (ValueType === 'SCOORD3D') {
for (let i = 0; i < GraphicData.length; i += 3) {
renderableData.push([GraphicData[i], GraphicData[i + 1], GraphicData[i + 2]]);
}
} else {
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [GraphicData[i], GraphicData[i + 1]]);
renderableData.push(worldPos);
}
}
return renderableData;
};
measurementData.renderableData[GraphicType].push(
_getRenderableData(GraphicType, GraphicData, imageId)
);
});
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
/**
* 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: {
toolName: toolName,
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
label: measurement.labels,
handles: {
textBox: measurement.textBox ?? {},
},
cachedStats: {},
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
frameNumber,
},
};
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation(SRAnnotation);
}
function _getRenderableData(GraphicType, GraphicData, imageId) {
let renderableData: csTypes.Point3[];
function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
let renderableData = [];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
renderableData = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
renderableData.push(worldPos);
}
case SCOORDTypes.POINT:
case SCOORDTypes.MULTIPOINT:
case SCOORDTypes.POLYLINE: {
renderableData = getRenderableCoords({ GraphicData, ValueType, imageId });
break;
case SCOORD_TYPES.CIRCLE: {
const pointsWorld = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
}
case SCOORDTypes.CIRCLE: {
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
GraphicData,
ValueType,
imageId,
});
// We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
const center = pointsWorld[0];
const onPerimeter = pointsWorld[1];
const radius = vec3.distance(center, onPerimeter);
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
if (!imagePlaneModule) {
throw new Error('No imagePlaneModule found');
}
@@ -115,14 +58,12 @@ function _getRenderableData(GraphicType, GraphicData, imageId) {
// we need to get major/minor axis (which are both the same size major = minor)
// first axisStart
const firstAxisStart = vec3.create();
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
const firstAxisEnd = vec3.create();
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
// second axisStart
const secondAxisStart = vec3.create();
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
@@ -138,21 +79,16 @@ function _getRenderableData(GraphicType, GraphicData, imageId) {
break;
}
case SCOORD_TYPES.ELLIPSE: {
case SCOORDTypes.ELLIPSE: {
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
// But Cornerstone3D points are ordered as top, bottom, left, right for the
// ellipse so we need to identify if the majorAxis is horizontal or vertical
// and then choose the correct points to use for the ellipse.
const pointsWorld: csTypes.Point3[] = [];
for (let i = 0; i < GraphicData.length; i += 2) {
const worldPos = utilities.imageToWorldCoords(imageId, [
GraphicData[i],
GraphicData[i + 1],
]);
pointsWorld.push(worldPos);
}
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
GraphicData,
ValueType,
imageId,
});
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
@@ -202,3 +138,5 @@ function _getRenderableData(GraphicType, GraphicData, imageId) {
return renderableData;
}
export default getRenderableData;
@@ -3,14 +3,12 @@ import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
const { locking } = CsAnnotation;
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
@@ -47,11 +45,19 @@ export default function hydrateStructuredReport(
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0];
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization('codingValues', {});
const { disableEditing } = customizationService.getCustomization(
'PanelMeasurement.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// TODO -> We should define a strict versioning somewhere.
@@ -4,7 +4,7 @@ const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG;
/**
* Checks if the given `displaySet`can be rehydrated into the `measurementService`.
@@ -3,6 +3,313 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **multiframe:** metadata handling of NM studies and loading order ([#4540](https://github.com/OHIF/Viewers/issues/4540)) ([71f06dc](https://github.com/OHIF/Viewers/commit/71f06dcd3e6d24f1e3d931f7db756c26038927da))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
### Bug Fixes
* video playback ([#4497](https://github.com/OHIF/Viewers/issues/4497)) ([610faa5](https://github.com/OHIF/Viewers/commit/610faa5a2738da5eabc40e57e338c359f481e852))
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
### Features
* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f))
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Features
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
### Features
* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58))
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
@@ -29,11 +29,11 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.9.0-beta.79",
"@ohif/extension-cornerstone": "3.9.0-beta.79",
"@ohif/extension-default": "3.9.0-beta.79",
"@ohif/i18n": "3.9.0-beta.79",
"@ohif/ui": "3.9.0-beta.79",
"@ohif/core": "3.9.2",
"@ohif/extension-cornerstone": "3.9.2",
"@ohif/extension-default": "3.9.2",
"@ohif/i18n": "3.9.2",
"@ohif/ui": "3.9.2",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -42,9 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.82.4",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
"@cornerstonejs/tools": "^1.82.4",
"@cornerstonejs/core": "^2.2.20",
"@cornerstonejs/tools": "^2.2.20",
"classnames": "^2.3.2"
}
}
@@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
return { timePoints, timePointsUnit };
}
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
function _getSegmentationData(
segmentation,
volumesTimePointsCache,
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
) {
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
const displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => {
@@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
referencedDynamicVolume.imageIds[0]
);
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const maskVolumeId = segmentationVolume?.volumeId;
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
maskVolumeId: segmentation.id,
maskVolumeId,
}) as number[][];
const pixelCount = timeData.length;
@@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
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 color = csTools.segmentation.config.color.getSegmentIndexColor(
// segmentationRepresentationUID,
// 1 // segmentIndex
// );
const viewportId = viewportGridService.getActiveViewportId();
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
const hexColor = cs.utilities.color.rgbToHex(...color);
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
if (!timePointsData) {
@@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
};
}
function _getInstanceFromSegmentations(segmentations, displaySetService) {
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
if (!segmentations.length) {
return;
}
const volumesTimePointsCache = new WeakMap();
const segmentationsData = segmentations.map(segmentation =>
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
);
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
@@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
}
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
const { segmentationService, displaySetService } = servicesManager.services;
debugger;
const { segmentationService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } =
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
if (seriesMetadata && instance) {
// An event is triggered after adding the instance and the displaySet is created
@@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const stdDeviation = Math.sqrt(variance);
return stdDeviation;
}
// Iterate through each segmentation to get the timeData and ijkCoords
segmentations.forEach((segmentation, segmentationIndex) => {
segmentations.forEach(segmentation => {
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
maskVolumeId: segmentation.id,
maskVolumeId: volume.volumeId,
}) as number[][];
if (summaryStats) {
@@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
// for each voxel in the ROI, get the value at the current time point
const voxelValues = [];
let sum = 0;
let minValue = Infinity;
let maxValue = -Infinity;
let minIndex = 0;
let maxIndex = 0;
// Single pass through the data to collect all needed values
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
voxelValues.push(timeData[voxelIndex][timeIndex]);
const value = timeData[voxelIndex][timeIndex];
voxelValues.push(value);
sum += value;
if (value < minValue) {
minValue = value;
minIndex = voxelIndex;
}
if (value > maxValue) {
maxValue = value;
maxIndex = voxelIndex;
}
}
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)]);
mean.push(sum / numVoxels);
min.push(minValue);
minIJK.push(ijkCoords[minIndex]);
max.push(maxValue);
maxIJK.push(ijkCoords[maxIndex]);
std.push(calculateStandardDeviation(voxelValues));
}
@@ -340,30 +356,19 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
throw new Error('No reference display set found based on the dynamic data');
}
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
referenceDisplaySet.displaySetInstanceUID,
{ label }
const displaySet = displaySetService.getDisplaySetByUID(
referenceDisplaySet.displaySetInstanceUID
);
// Add Segmentation to all toolGroupIds in the viewer
const toolGroupIds = Array.from(
viewports.values(),
viewport => viewport.viewportOptions.toolGroupId
);
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
label,
});
const representationType = LABELMAP;
const firstViewport = viewports.values().next().value;
for (const toolGroupId of toolGroupIds) {
const hydrateSegmentation = true;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId,
hydrateSegmentation,
representationType
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
segmentationId,
});
return segmentationId;
},
@@ -147,8 +147,8 @@ function getFusionViewports() {
name: DEFAULT_COLORMAP,
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.3 },
{ value: 1, opacity: 0.3 },
{ value: 0.1, opacity: 0.8 },
{ value: 1, opacity: 0.8 },
],
},
};
@@ -191,6 +191,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -240,6 +249,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -289,6 +307,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -1,9 +1,10 @@
import React from 'react';
import { DynamicDataPanel } from './panels';
import { Toolbox } from '@ohif/ui';
import { Toolbox } from '@ohif/ui-next';
import { PanelSegmentation } from '@ohif/extension-cornerstone';
import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
const wrappedDynamicDataPanel = () => {
return (
<DynamicDataPanel
@@ -14,7 +15,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
);
};
const wrappedDynamicToolbox = () => {
const wrappedDynamicSegmentation = () => {
return (
<>
<Toolbox
@@ -24,18 +25,17 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
buttonSectionId="dynamic-toolbox"
title="Threshold Tools"
/>
</>
);
};
const wrappedDynamicExport = () => {
return (
<>
<DynamicExport
commandsManager={commandsManager}
<PanelSegmentation
servicesManager={servicesManager}
commandsManager={commandsManager}
extensionManager={extensionManager}
/>
configuration={configuration}
>
<DynamicExport
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PanelSegmentation>
</>
);
};
@@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
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,
name: 'dynamic-segmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedDynamicSegmentation,
},
];
}
@@ -1,17 +1,19 @@
import React from 'react';
import PanelGenerateImage from './PanelGenerateImage';
function DynamicDataPanel({ servicesManager, commandsManager }) {
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
return (
<div
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
<>
<div
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage
commandsManager={commandsManager}
servicesManager={servicesManager}
></PanelGenerateImage>
</div>
</>
);
}
@@ -1,73 +1,52 @@
import React, { useState, useEffect } from 'react';
import { ActionButtons } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import { useSegmentations } from '@ohif/extension-cornerstone';
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('dynamicExport');
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
const segmentations = useSegmentations({ servicesManager });
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();
});
};
}, []);
if (!segmentations?.length) {
return null;
}
return (
<div>
<div className="mt-3 flex justify-center px-2">
<ActionButtons
actions={actions}
t={t}
/>
<div className="flex gap-2">
<div className="flex h-8 w-full items-center rounded pr-0.5">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
options: {
filename: 'TimeData.csv',
},
});
}}
>
<Icons.Export />
<span className="pl-1">Time Data</span>
</Button>
</div>
<div className="flex h-8 w-full items-center rounded pr-0.5">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
summaryStats: true,
options: {
filename: 'ROIStats.csv',
},
});
}}
>
<Icons.Export />
<span className="pl-1">ROI Stats</span>
</Button>
</div>
</div>
);
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import {
InputDoubleRange,
Button,
PanelSection,
ButtonGroup,
@@ -10,6 +9,8 @@ import {
Tooltip,
} from '@ohif/ui';
import { DoubleSlider } from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core';
const controlClassNames = {
@@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
useEffect(() => {
setSliderRangeValues([framesLength / 4, framesLength / 2]);
}, [framesLength]);
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
const handleSliderChange = newValues => {
onDoubleRangeChange(newValues);
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
return;
}
setSliderRangeValues(newValues);
};
const formatLabel = value => Math.round(value);
return (
<div className="flex select-none flex-col">
<PanelSection
@@ -134,7 +129,7 @@ const DynamicVolumeControls = ({
}
/>
<ButtonGroup
className={`mt-2 w-full `}
className={`mt-2 w-full`}
separated={true}
>
<button
@@ -156,15 +151,15 @@ const DynamicVolumeControls = ({
{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}
<div className="mt-2 w-full">
<DoubleSlider
min={0}
max={framesLength - 1}
step={1}
defaultValue={sliderRangeValues}
onValueChange={handleSliderChange}
formatLabel={formatLabel}
className="w-full"
/>
</div>
<Button
@@ -216,14 +211,14 @@ function FrameControls({
>
<Icon
name={getPlayPauseIconName()}
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
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}
maxValue={framesLength - 1}
label="Frame"
{...controlClassNames}
/>
@@ -1,8 +1,6 @@
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 csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
import { utilities as cstUtils } from '@cornerstonejs/tools';
import DynamicVolumeControls from './DynamicVolumeControls';
@@ -16,8 +14,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const [{ activeViewportId }] = useViewportGrid();
//
const [timePointsRange, setTimePointsRange] = useState([]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
const [dynamicVolume, setDynamicVolume] = useState(null);
const [frameRate, setFrameRate] = useState(20);
@@ -31,47 +29,62 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
useEffect(() => {
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
const viewportDataChangedCallback = evtDetails => {
evtDetails.viewportData.data.forEach(volumeData => {
if (volumeData.volume.isDynamicVolume()) {
if (volumeData.volume?.isDynamicVolume()) {
setDynamicVolume(volumeData.volume);
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
setTimePointsRange([1, volumeData.volume.numTimePoints]);
const newRange = [1, volumeData.volume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
}
});
});
};
const cineStateChangedCallback = evt => {
setIsPlaying(evt.isPlaying);
};
const { unsubscribe: unsubscribeViewportData } = cornerstoneViewportService.subscribe(
viewportDataChangedEvt,
viewportDataChangedCallback
);
const { unsubscribe: unsubscribeCineState } = servicesManager.services.cineService.subscribe(
cineStateChangedEvt,
cineStateChangedCallback
);
return () => {
unsubscribe();
unsubscribeViewportData();
unsubscribeCineState();
};
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
useEffect(() => {
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]);
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) {
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetUID =>
displaySetService.getDisplaySetByUID(displaySetUID)
);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
if (!dynamicVolumeDisplaySet) {
@@ -88,23 +101,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
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]);
const newRange = [1, dynamicVolume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
}, [
activeViewportId,
viewportGridService,
displaySetService,
cornerstoneViewportService,
cineService,
]);
function renderGeneratedImage(displaySet) {
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
@@ -130,26 +136,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
let computedVolume = cache.getVolume(computedVolumeId);
if (!computedVolume) {
await createComputedVolume(dynamicVolumeId, computedVolumeId);
computedVolume = cache.getVolume(computedVolumeId);
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
volumeId: computedVolumeId,
});
}
const [start, end] = timePointsRangeToUseForGenerate;
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
const vals = timePointsRangeToUseForGenerate;
const options = {
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
targetVolume: computedVolume,
};
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);
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
// 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
@@ -166,6 +165,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
madeInClient: true,
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
isDerived: true,
imageIds: computedVolume.imageIds,
};
setComputedDisplaySet(displaySet);
renderGeneratedImage(displaySet);
@@ -173,19 +173,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
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);
// }
}
}
@@ -248,28 +237,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
onGenerate={onGenerateImage}
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
onDoubleRangeChange={handleSliderChange}
initialRangeValues={timePointsRangeToUseForGenerate}
/>
);
}
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,
};
+317
View File
@@ -3,6 +3,323 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.9.2](https://github.com/OHIF/Viewers/compare/v3.9.1...v3.9.2) (2024-12-03)
### Bug Fixes
* **modes:** don't attempt to retrieve a stage index if HPs ([#4552](https://github.com/OHIF/Viewers/issues/4552)) ([e5286d9](https://github.com/OHIF/Viewers/commit/e5286d9fc2449da50b738e7e6208d489cce1633e))
* **multiframe:** metadata handling of NM studies and loading order ([#4540](https://github.com/OHIF/Viewers/issues/4540)) ([71f06dc](https://github.com/OHIF/Viewers/commit/71f06dcd3e6d24f1e3d931f7db756c26038927da))
## [3.9.1](https://github.com/OHIF/Viewers/compare/v3.9.0...v3.9.1) (2024-11-13)
### Bug Fixes
* video playback ([#4497](https://github.com/OHIF/Viewers/issues/4497)) ([610faa5](https://github.com/OHIF/Viewers/commit/610faa5a2738da5eabc40e57e338c359f481e852))
# [3.9.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
### Bug Fixes
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
### Bug Fixes
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
### Bug Fixes
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
### Bug Fixes
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
### Bug Fixes
* **ui:** show ui notification on displaySet load error ([#4447](https://github.com/OHIF/Viewers/issues/4447)) ([4f20523](https://github.com/OHIF/Viewers/commit/4f20523109ecbb7ec5a6d5f2c97f7e73f81cda09))
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
### Bug Fixes
* **ui:** display error in ui while loading seg ([#4433](https://github.com/OHIF/Viewers/issues/4433)) ([2e96371](https://github.com/OHIF/Viewers/commit/2e96371b0631a9e5d411b0142300708ab8ba7d27))
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
### Bug Fixes
* **3d-viewport:** exception was being thrown and 3d viewpot not getting resized. ([#4366](https://github.com/OHIF/Viewers/issues/4366)) ([433cc80](https://github.com/OHIF/Viewers/commit/433cc8089db6aa218c9075bd0eeb7952a7e4f028))
* **hp-presets:** select the active displaySet when toggling an HP preset, not a random one from the series panel ([#4365](https://github.com/OHIF/Viewers/issues/4365)) ([ace67b3](https://github.com/OHIF/Viewers/commit/ace67b3bbb6be4e8c78e613e20d3e10b93762bf7))
* **tools:** check if seriesNumber is an undefined properly ([#4338](https://github.com/OHIF/Viewers/issues/4338)) ([307b144](https://github.com/OHIF/Viewers/commit/307b14476be41b10b861d6a8474f7386b5107618))
* **typo:** type in fourup preset ([#4426](https://github.com/OHIF/Viewers/issues/4426)) ([03aad4e](https://github.com/OHIF/Viewers/commit/03aad4eba24e33a266a6d91eaf74df52dc2a550e))
* **updateIndex:** getNumberOfSlices is defined when used with a 3D viewport ([#4424](https://github.com/OHIF/Viewers/issues/4424)) ([d5bcf54](https://github.com/OHIF/Viewers/commit/d5bcf54e23ef68abd85c5f0ea671feca637c4f49))
### Features
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
+10 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.9.0-beta.79",
"version": "3.9.2",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -36,12 +36,12 @@
"peerDependencies": {
"@cornerstonejs/codec-charls": "^1.2.3",
"@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.82.4",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^2.2.20",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.9.0-beta.79",
"@ohif/ui": "3.9.0-beta.79",
"@ohif/core": "3.9.2",
"@ohif/ui": "3.9.2",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -55,12 +55,11 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.82.4",
"@cornerstonejs/core": "^1.82.4",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
"@cornerstonejs/tools": "^1.82.4",
"@cornerstonejs/adapters": "^2.2.20",
"@cornerstonejs/core": "^2.2.20",
"@cornerstonejs/tools": "^2.2.20",
"@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "30.4.1",
"@kitware/vtk.js": "32.1.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
@@ -1,10 +1,9 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import PropTypes from 'prop-types';
import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
import { AllInOneMenu, Notification, useViewportDialog } from '@ohif/ui';
import type { Types as csTypes } from '@cornerstonejs/core';
import { setEnabledElement } from '../state';
@@ -17,8 +16,10 @@ import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state';
import { LutPresentation, PositionPresentation } from '../types/Presentation';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
const STACK = 'stack';
@@ -28,428 +29,401 @@ const STACK = 'stack';
*/
let cacheJumpToMeasurementEvent;
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
return false;
}
if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
return false;
}
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
return false;
}
}
}
}
return true;
}
// 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: withAppTypes) => {
const {
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
isJumpToMeasurementDisabled = false,
// 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,
// 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 OHIFCornerstoneViewport = React.memo(
(
props: withAppTypes<{
viewportId: string;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions;
initialImageIndex: number;
}>
) => {
const {
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
isJumpToMeasurementDisabled = false,
// 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,
// 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;
const viewportId = viewportOptions.viewportId;
if (!viewportId) {
throw new Error('Viewport ID is required');
}
// Make sure displaySetOptions has one object per displaySet
while (displaySetOptions.length < displaySets.length) {
displaySetOptions.push({});
}
// 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() as React.MutableRefObject<HTMLDivElement>;
const [appConfig] = useAppConfig();
const {
displaySetService,
toolbarService,
toolGroupService,
syncGroupService,
cornerstoneViewportService,
cornerstoneCacheService,
stateSyncService,
viewportActionCornersService,
} = servicesManager.services;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
if (!viewportId) {
throw new Error('Viewport ID is required');
}
}, [elementRef]);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
// Make sure displaySetOptions has one object per displaySet
while (displaySetOptions.length < displaySets.length) {
displaySetOptions.push({});
}
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
// 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;
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const [appConfig] = useAppConfig();
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
const {
displaySetService,
toolbarService,
toolGroupService,
syncGroupService,
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
} = servicesManager.services;
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}, [elementRef]);
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
segmentationService.clearSegmentationRepresentations(viewportId);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportActionCornersService.clear(viewportId);
},
[
viewportId,
segmentationService,
syncGroupService,
toolGroupService,
viewportActionCornersService,
]
);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
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);
}
},
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) {
return;
}
cornerstoneViewportService.storePresentation({ viewportId });
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
}
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
// we don't need reactivity here so just use state
const { synchronizersStore } = useSynchronizersStore.getState();
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(viewportId, syncGroupService);
}
if (onElementEnabled && typeof onElementEnabled === 'function') {
onElementEnabled(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
if (!viewportInfo) {
return;
}
}
);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
cornerstoneViewportService.storePresentation({ viewportId });
const loadViewportData = async () => {
const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
// The presentation state will have been stored previously by closing
// a viewport. Otherwise, this viewport will be unchanged and the
// presentation information will be directly carried over.
const state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
if (onElementDisabled && typeof onElementDisabled === 'function') {
onElementDisabled(viewportInfo);
}
const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
}, []);
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
return;
}
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
}
}
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
};
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
const loadViewportData = async () => {
const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const presentations = getViewportPresentations(viewportId, viewportOptions);
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId]);
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
};
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
const wlActionMenu = getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
const location = viewportActionCornersService.LOCATIONS.topRight;
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
viewportActionCornersService.addComponent({
viewportId,
id: 'windowLevelActionMenu',
component: getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
}),
location,
});
viewportActionCornersService.addComponent({
viewportId,
id: 'segmentation',
component: getViewportDataOverlaySettingsMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location,
}),
location,
});
}, [
displaySets,
viewportId,
viewportActionCornersService,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
appConfig,
]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
return (
<React.Fragment>
<div className="viewport-wrapper">
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
return (
<React.Fragment>
<div className="viewport-wrapper">
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
}, areEqual);
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
<ActiveViewportBehavior
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
},
areEqual
);
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
@@ -542,12 +516,15 @@ function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesM
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId];
if (!synchronizers) {
return;
}
synchronizers.forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
@@ -597,18 +574,91 @@ function _rehydrateSynchronizers(
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.propTypes = {
displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func,
isJumpToMeasurementDisabled: PropTypes.bool,
// 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
initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
return false;
}
if (
nextProps.viewportOptions.viewportType &&
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
return false;
}
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
return false;
}
}
}
}
return true;
}
// Helper function to check if display sets have changed
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
if (prevDisplaySets.length !== currentDisplaySets.length) {
return true;
}
return currentDisplaySets.some((currentDS, index) => {
const prevDS = prevDisplaySets[index];
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
});
}
export default OHIFCornerstoneViewport;
@@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui';
import { InstanceMetadata } from '@ohif/core/src/types';
import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -45,14 +45,14 @@ const OverlayItemComponents = {
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate),
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
};
const seriesDescriptionItem = {
@@ -63,15 +63,19 @@ const seriesDescriptionItem = {
condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription;
},
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
};
const topLeftItems = { id: 'cornerstoneOverlayTopLeft', items: [studyDateItem, seriesDescriptionItem] };
const topLeftItems = {
id: 'cornerstoneOverlayTopLeft',
items: [studyDateItem, seriesDescriptionItem],
};
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft', items: [
id: 'cornerstoneOverlayBottomLeft',
items: [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
@@ -79,12 +83,12 @@ const bottomLeftItems = {
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: (props) => {
condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom';
},
},
]
],
};
const bottomRightItems = {
@@ -94,7 +98,7 @@ const bottomRightItems = {
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
]
],
};
/**
@@ -145,19 +149,18 @@ function CustomizableViewportOverlay({
// append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items.
const topLeftCustomization = customizationService.getCustomization(
'cornerstoneOverlayTopLeft'
) || cornerstoneOverlay?.topLeftItems;
const topRightCustomization = customizationService.getCustomization(
'cornerstoneOverlayTopRight'
) || cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization = customizationService.getCustomization(
'cornerstoneOverlayBottomLeft'
) || cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization = customizationService.getCustomization(
'cornerstoneOverlayBottomRight'
) || cornerstoneOverlay?.bottomRightItems;
const topLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
cornerstoneOverlay?.topLeftItems;
const topRightCustomization =
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
cornerstoneOverlay?.bottomRightItems;
const instanceNumber = useMemo(
() =>
@@ -177,13 +180,12 @@ function CustomizableViewportOverlay({
return {
displaySets,
displaySet,
instance: instances[imageIndex],
instance: instances?.[imageIndex],
instances,
referenceInstance,
};
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
/**
* Updating the VOI when the viewport changes its voi
*/
@@ -307,7 +309,8 @@ function CustomizableViewportOverlay({
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
null}
</div>
))}
</>
@@ -316,7 +319,6 @@ function CustomizableViewportOverlay({
[_renderOverlayItem]
);
return (
<ViewportOverlay
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
@@ -335,7 +337,9 @@ function getDisplaySets(viewportData, displaySetService) {
if (!viewportData?.data?.length) {
return null;
}
const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
const displaySets = viewportData.data
.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID))
.filter(it => !!it);
if (!displaySets.length) {
return null;
}
@@ -382,7 +386,6 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
@@ -400,6 +403,11 @@ function _getInstanceNumberFromVolume(
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
if (!volume) {
return;
}
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@@ -443,9 +451,10 @@ function OverlayItem(props) {
style={{ color, background }}
title={title}
>
{label ? (<span className="mr-1 shrink-0">{label}</span>) : null}
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-1 mr-2 shrink-0">{value}</span>
</div>);
</div>
);
}
/**
@@ -1,7 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, Types, utilities } from '@cornerstonejs/core';
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui';
function CornerstoneImageScrollbar({
@@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({
setImageSliceData,
scrollbarHeight,
servicesManager,
}: withAppTypes) {
}: withAppTypes<{
element: HTMLElement;
}>) {
const { cineService, cornerstoneViewportService } = servicesManager.services;
const onImageScrollbarChange = (imageIndex, viewportId) => {
@@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({
cineService.setCine({ id: viewportId, isPlaying: false });
}
csToolsUtils.jumpToSlice(viewport.element, {
csUtils.jumpToSlice(viewport.element, {
imageIndex,
debounceLoading: true,
});
@@ -39,7 +40,7 @@ function CornerstoneImageScrollbar({
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
@@ -64,6 +65,9 @@ function CornerstoneImageScrollbar({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds
@@ -1,14 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react';
import classNames from 'classnames';
import {
metaData,
Enums,
Types,
getEnabledElement,
utilities as csUtils,
} from '@cornerstonejs/core';
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css';
@@ -31,8 +24,9 @@ function ViewportOrientationMarkers({
useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { rotation, previousCamera, camera } = evt.detail;
const { previousCamera, camera } = evt.detail;
const { rotation } = camera;
if (rotation !== undefined) {
setRotation(rotation);
}
@@ -74,7 +68,12 @@ function ViewportOrientationMarkers({
return false;
}
({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {});
({
rowCosines,
columnCosines,
isDefaultValueSetForColumnCosine,
isDefaultValueSetForColumnCosine,
} = metaData.get('imagePlaneModule', imageId) || {});
} else {
if (!element || !getEnabledElement(element)) {
return '';
@@ -90,7 +89,13 @@ function ViewportOrientationMarkers({
rowCosines = viewRight;
}
if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) {
if (
!rowCosines ||
!columnCosines ||
rotation === undefined ||
isDefaultValueSetForRowCosine ||
isDefaultValueSetForColumnCosine
) {
return '';
}
+481 -15
View File
@@ -12,15 +12,23 @@ import {
utilities as cstUtils,
ReferenceLinesTool,
} from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core';
import {
callLabelAutocompleteDialog,
showLabelAnnotationPopup,
createReportAsync,
callInputDialog,
colorPickerDialog,
} from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
@@ -29,6 +37,7 @@ const toggleSyncFunctions = {
function commandsModule({
servicesManager,
extensionManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const {
@@ -101,7 +110,47 @@ function commandsModule({
commandsManager.run(options, optionsToUse);
},
updateStoredSegmentationPresentation: ({ displaySet, type }) => {
const { addSegmentationPresentationItem } = useSegmentationPresentationStore.getState();
const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
addSegmentationPresentationItem(referencedDisplaySetInstanceUID, {
segmentationId: displaySet.displaySetInstanceUID,
hydrated: true,
type,
});
},
updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUID }) => {
const presentations = cornerstoneViewportService.getPresentations(viewportId);
const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } =
usePositionPresentationStore.getState();
// Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID
// and the value has viewportId as activeViewportId.
const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId
)?.[0];
if (previousReferencedDisplaySetStoreKey) {
setPositionPresentation(
previousReferencedDisplaySetStoreKey,
presentations.positionPresentation
);
return;
}
// if not found means we have not visited that referencedDisplaySetInstanceUID before
// so we need to grab the positionPresentationId directly from the store,
// Todo: this is really hacky, we should have a better way for this
const positionPresentationId = getPositionPresentationId({
displaySetInstanceUIDs: [displaySetInstanceUID],
viewportId,
});
setPositionPresentation(positionPresentationId, presentations.positionPresentation);
},
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
},
@@ -435,9 +484,10 @@ function commandsModule({
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render();
} else if (viewport.getRotation !== undefined) {
const currentRotation = viewport.getRotation();
const presentation = viewport.getViewPresentation();
const { rotation: currentRotation } = presentation;
const newRotation = (currentRotation + rotation) % 360;
viewport.setProperties({ rotation: newRotation });
viewport.setViewPresentation({ rotation: newRotation });
viewport.render();
}
},
@@ -554,7 +604,7 @@ function commandsModule({
// Set slice to last slice
const options = { imageIndex: jumpIndex };
cstUtils.jumpToSlice(viewport.element, options);
csUtils.jumpToSlice(viewport.element, options);
},
scroll: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -566,7 +616,7 @@ function commandsModule({
const { viewport } = enabledElement;
const options = { delta: direction };
cstUtils.scroll(viewport, options);
csUtils.scroll(viewport, options);
},
setViewportColormap: ({
viewportId,
@@ -577,7 +627,6 @@ function commandsModule({
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
@@ -596,14 +645,8 @@ function commandsModule({
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
};
if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId);
viewport.setProperties({ colormap });
}
if (viewport instanceof VolumeViewport) {
@@ -611,7 +654,9 @@ function commandsModule({
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
setViewportProperties(viewport, displaySetInstanceUID);
const volumeId = viewport.getVolumeId();
viewport.setProperties({ colormap }, volumeId);
}
if (immediate) {
@@ -824,9 +869,352 @@ function commandsModule({
}
crosshairInstances.forEach(ins => {
ins?.resetCrosshairs();
ins?.computeToolCenter();
});
},
/**
* Creates a labelmap for the active viewport
*/
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
const { viewportGridService, displaySetService, segmentationService } =
servicesManager.services;
const { viewports } = viewportGridService.getState();
const targetViewportId = viewportId;
const viewport = viewports.get(targetViewportId);
// Todo: add support for multiple display sets
const displaySetInstanceUID =
options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
const segs = segmentationService.getSegmentations();
const label = options.label || `Segmentation ${segs.length + 1}`;
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
displaySet,
{
label,
segmentationId,
segments: options.createInitialSegment
? {
1: {
label: 'Segment 1',
active: true,
},
}
: {},
}
);
await segmentationService.addSegmentationRepresentation(viewportId, {
segmentationId,
type: Enums.SegmentationRepresentations.Labelmap,
});
return generatedSegmentationId;
},
/**
* Sets the active segmentation for a viewport
* @param props.segmentationId - The ID of the segmentation to set as active
*/
setActiveSegmentation: ({ segmentationId }) => {
const { viewportGridService, segmentationService } = servicesManager.services;
segmentationService.setActiveSegmentation(
viewportGridService.getActiveViewportId(),
segmentationId
);
},
/**
* Adds a new segment to a segmentation
* @param props.segmentationId - The ID of the segmentation to add the segment to
*/
addSegmentCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.addSegment(segmentationId);
},
/**
* Sets the active segment and jumps to its center
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment to activate
*/
setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setActiveSegment(segmentationId, segmentIndex);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
},
/**
* Toggles the visibility of a segment
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment
* @param props.type - The type of visibility to toggle
*/
toggleSegmentVisibilityCommand: ({ segmentationId, segmentIndex, type }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
segmentationService.toggleSegmentVisibility(
viewportGridService.getActiveViewportId(),
segmentationId,
segmentIndex,
type
);
},
/**
* Toggles the lock state of a segment
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment
*/
toggleSegmentLockCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
},
/**
* Toggles the visibility of a segmentation representation
* @param props.segmentationId - The ID of the segmentation
* @param props.type - The type of representation
*/
toggleSegmentationVisibilityCommand: ({ segmentationId, type }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
segmentationService.toggleSegmentationRepresentationVisibility(
viewportGridService.getActiveViewportId(),
{ segmentationId, type }
);
},
/**
* Downloads a segmentation
* @param props.segmentationId - The ID of the segmentation to download
*/
downloadSegmentationCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.downloadSegmentation(segmentationId);
},
/**
* Stores a segmentation and shows it in the viewport
* @param props.segmentationId - The ID of the segmentation to store
*/
storeSegmentationCommand: async ({ segmentationId }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
if (displaySetInstanceUIDs) {
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
},
/**
* Downloads a segmentation as RTSS
* @param props.segmentationId - The ID of the segmentation
*/
downloadRTSSCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.downloadRTSS(segmentationId);
},
/**
* Sets the style for a segmentation
* @param props.segmentationId - The ID of the segmentation
* @param props.type - The type of style
* @param props.key - The style key to set
* @param props.value - The style value
*/
setSegmentationStyleCommand: ({ type, key, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { [key]: value });
},
/**
* Deletes a segment from a segmentation
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment to delete
*/
deleteSegmentCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
segmentationService.removeSegment(segmentationId, segmentIndex);
},
/**
* Deletes an entire segmentation
* @param props.segmentationId - The ID of the segmentation to delete
*/
deleteSegmentationCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.remove(segmentationId);
},
/**
* Removes a segmentation from the viewport
* @param props.segmentationId - The ID of the segmentation to remove
*/
removeSegmentationFromViewportCommand: ({ segmentationId }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
segmentationService.removeSegmentationRepresentations(
viewportGridService.getActiveViewportId(),
{ segmentationId }
);
},
/**
* Toggles rendering of inactive segmentations
*/
toggleRenderInactiveSegmentationsCommand: () => {
const { segmentationService, viewportGridService } = servicesManager.services;
const viewportId = viewportGridService.getActiveViewportId();
const renderInactive = segmentationService.getRenderInactiveSegmentations(viewportId);
segmentationService.setRenderInactiveSegmentations(viewportId, !renderInactive);
},
/**
* Sets the fill alpha value for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - The alpha value to set
*/
setFillAlphaCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { fillAlpha: value });
},
/**
* Sets the outline width for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - The width value to set
*/
setOutlineWidthCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { outlineWidth: value });
},
/**
* Sets whether to render fill for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - Whether to render fill
*/
setRenderFillCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { renderFill: value });
},
/**
* Sets whether to render outline for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - Whether to render outline
*/
setRenderOutlineCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { renderOutline: value });
},
/**
* Sets the fill alpha for inactive segmentations
* @param props.type - The type of segmentation
* @param props.value - The alpha value to set
*/
setFillAlphaInactiveCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { fillAlphaInactive: value });
},
editSegmentLabel: ({ segmentationId, segmentIndex }) => {
const { segmentationService, uiDialogService } = servicesManager.services;
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
const callback = (label, actionId) => {
if (label === '') {
return;
}
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
};
callInputDialog(uiDialogService, label, callback, false, {
dialogTitle: 'Edit Segment Label',
inputLabel: 'Enter new label',
});
},
editSegmentationLabel: ({ segmentationId }) => {
const { segmentationService, uiDialogService } = servicesManager.services;
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const { label } = segmentation;
const callback = (label, actionId) => {
if (label === '') {
return;
}
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
};
callInputDialog(uiDialogService, label, callback, false, {
dialogTitle: 'Edit Segmentation Label',
inputLabel: 'Enter new label',
});
},
editSegmentColor: ({ segmentationId, segmentIndex }) => {
const { segmentationService, uiDialogService, viewportGridService } =
servicesManager.services;
const viewportId = viewportGridService.getActiveViewportId();
const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex);
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: color[3] / 255.0,
};
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
});
},
getRenderInactiveSegmentations: () => {
const { segmentationService, viewportGridService } = servicesManager.services;
return segmentationService.getRenderInactiveSegmentations(
viewportGridService.getActiveViewportId()
);
},
};
const definitions = {
@@ -988,6 +1376,84 @@ function commandsModule({
toggleActiveDisabledToolbar: {
commandFn: actions.toggleActiveDisabledToolbar,
},
updateStoredPositionPresentation: {
commandFn: actions.updateStoredPositionPresentation,
},
updateStoredSegmentationPresentation: {
commandFn: actions.updateStoredSegmentationPresentation,
},
createLabelmapForViewport: {
commandFn: actions.createLabelmapForViewport,
},
setActiveSegmentation: {
commandFn: actions.setActiveSegmentation,
},
addSegment: {
commandFn: actions.addSegmentCommand,
},
setActiveSegmentAndCenter: {
commandFn: actions.setActiveSegmentAndCenterCommand,
},
toggleSegmentVisibility: {
commandFn: actions.toggleSegmentVisibilityCommand,
},
toggleSegmentLock: {
commandFn: actions.toggleSegmentLockCommand,
},
toggleSegmentationVisibility: {
commandFn: actions.toggleSegmentationVisibilityCommand,
},
downloadSegmentation: {
commandFn: actions.downloadSegmentationCommand,
},
storeSegmentation: {
commandFn: actions.storeSegmentationCommand,
},
downloadRTSS: {
commandFn: actions.downloadRTSSCommand,
},
setSegmentationStyle: {
commandFn: actions.setSegmentationStyleCommand,
},
deleteSegment: {
commandFn: actions.deleteSegmentCommand,
},
deleteSegmentation: {
commandFn: actions.deleteSegmentationCommand,
},
removeSegmentationFromViewport: {
commandFn: actions.removeSegmentationFromViewportCommand,
},
toggleRenderInactiveSegmentations: {
commandFn: actions.toggleRenderInactiveSegmentationsCommand,
},
setFillAlpha: {
commandFn: actions.setFillAlphaCommand,
},
setOutlineWidth: {
commandFn: actions.setOutlineWidthCommand,
},
setRenderFill: {
commandFn: actions.setRenderFillCommand,
},
setRenderOutline: {
commandFn: actions.setRenderOutlineCommand,
},
setFillAlphaInactive: {
commandFn: actions.setFillAlphaInactiveCommand,
},
editSegmentLabel: {
commandFn: actions.editSegmentLabel,
},
editSegmentationLabel: {
commandFn: actions.editSegmentationLabel,
},
editSegmentColor: {
commandFn: actions.editSegmentColor,
},
getRenderInactiveSegmentations: {
commandFn: actions.getRenderInactiveSegmentations,
},
};
return {
@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
import { useAppConfig } from '@state';
function WrappedCinePlayer({
@@ -101,7 +100,7 @@ function WrappedCinePlayer({
return;
}
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
enabledVPElement.addEventListener(Enums.Events.VIEWPORT_NEW_IMAGE_SET, newDisplaySetHandler);
// this doesn't makes sense that we are listening to this event on viewport element
enabledVPElement.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
@@ -111,7 +110,10 @@ function WrappedCinePlayer({
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
enabledVPElement.removeEventListener(
Enums.Events.VIEWPORT_NEW_IMAGE_SET,
newDisplaySetHandler
);
enabledVPElement.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
@@ -181,13 +183,13 @@ function RenderCinePlayer({
};
eventTarget.addEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
return () => {
eventTarget.removeEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
};
@@ -199,7 +201,7 @@ function RenderCinePlayer({
}
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
const volume = cache.getVolume(volumeId);
const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
@@ -207,7 +209,7 @@ function RenderCinePlayer({
const updateDynamicInfo = useCallback(props => {
const { volumeId, timePointIndex } = props;
const volume = cache.getVolume(volumeId);
const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex;
}, []);
@@ -223,6 +225,7 @@ function RenderCinePlayer({
isPlaying: false,
});
cineService.setIsCineEnabled(false);
cineService.setViewportCineClosed(viewportId);
}}
onPlayPauseChange={isPlaying => {
cineService.setCine({
@@ -0,0 +1,136 @@
import React, { useEffect, useState } from 'react';
import { Button, Icons, Separator } from '@ohif/ui-next';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
function ViewportSegmentationMenu({
viewportId,
servicesManager,
}: withAppTypes<{ viewportId: string }>) {
const { segmentationService } = servicesManager.services;
const [activeSegmentations, setActiveSegmentations] = useState([]);
const [availableSegmentations, setAvailableSegmentations] = useState([]);
useEffect(() => {
const updateSegmentations = () => {
const active = segmentationService.getSegmentationRepresentations(viewportId);
setActiveSegmentations(active);
const all = segmentationService.getSegmentations();
const available = all.filter(
seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId)
);
setAvailableSegmentations(available);
};
updateSegmentations();
const subscriptions = [
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
].map(event => segmentationService.subscribe(event, updateSegmentations));
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
};
}, [segmentationService, viewportId]);
const toggleSegmentationRepresentationVisibility = (
segmentationId,
type = SegmentationRepresentations.Labelmap
) => {
segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
segmentationId,
type,
});
};
const addSegmentationToViewport = segmentationId => {
segmentationService.addSegmentationRepresentation(viewportId, { segmentationId });
};
const removeSegmentationFromViewport = segmentationId => {
segmentationService.removeSegmentationRepresentations(viewportId, {
segmentationId,
});
};
return (
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
<ul className="space-y-1">
{activeSegmentations.map(segmentation => (
<li
key={segmentation.id}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
>
<Icons.Minus className="h-6 w-6" />
</Button>
<span className="text-foreground flex-grow">{segmentation.label}</span>
{segmentation.visible ? (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Hide className="h-6 w-6" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Show className="h-6 w-6" />
</Button>
)}
</li>
))}
</ul>
{availableSegmentations.length > 0 && (
<>
<Separator className="bg-input mb-3" />
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
<ul className="space-y-1">
{availableSegmentations.map(({segmentationId, label}) => (
<li
key={segmentationId}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => addSegmentationToViewport(segmentationId)}
>
<Icons.Plus className="h-6 w-6" />
</Button>
<span className="text-foreground/60">{label}</span>
</li>
))}
</ul>
</>
)}
</div>
);
}
export default ViewportSegmentationMenu;
@@ -0,0 +1,83 @@
import React, { ReactNode, useEffect, useState } from 'react';
import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next';
import ViewportSegmentationMenu from './ViewportSegmentationMenu';
import classNames from 'classnames';
import { useSegmentations } from '../../hooks/useSegmentations';
export function ViewportSegmentationMenuWrapper({
viewportId,
displaySets,
servicesManager,
commandsManager,
location,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>): ReactNode {
const { viewportActionCornersService, viewportGridService } = servicesManager.services;
const segmentations = useSegmentations({ servicesManager });
const activeViewportId = viewportGridService.getActiveViewportId();
const isActiveViewport = viewportId === activeViewportId;
const { align, side } = getAlignAndSide(viewportActionCornersService, location);
if (!segmentations?.length) {
return null;
}
return (
<Popover>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
>
<Icons.ViewportViews
className={classNames(
'text-highlight',
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={-15}
sideOffset={5}
>
<ViewportSegmentationMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PopoverContent>
</Popover>
);
}
const getAlignAndSide = (viewportActionCornersService, location) => {
const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS;
switch (location) {
case ViewportActionCornersLocations.topLeft:
return { align: 'start', side: 'bottom' };
case ViewportActionCornersLocations.topRight:
return { align: 'end', side: 'bottom' };
case ViewportActionCornersLocations.bottomLeft:
return { align: 'start', side: 'top' };
case ViewportActionCornersLocations.bottomRight:
return { align: 'end', side: 'top' };
default:
console.debug('Unknown location, defaulting to bottom-start');
return { align: 'start', side: 'bottom' };
}
};
@@ -0,0 +1,11 @@
import React, { ReactNode } from 'react';
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
export function getViewportDataOverlaySettingsMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>
): ReactNode {
return <ViewportSegmentationMenuWrapper {...props} />;
}
@@ -1,10 +1,15 @@
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
import React, { useEffect, useCallback, useState, ReactElement, useMemo } 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';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
getNodeOpacity,
isPetVolumeWithDefaultOpacity,
isVolumeWithConstantOpacity,
getWindowLevelsData,
} from './utils';
const { Events } = Enums;
@@ -16,19 +21,16 @@ const ViewportWindowLevel = ({
}>): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]);
const [cachedHistograms, setCachedHistograms] = useState({});
const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets({ servicesManager });
/**
* 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);
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@@ -38,97 +40,10 @@ const ViewportWindowLevel = ({
[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;
const volumeActor = viewport.getActors().find(actor => actor.referencedId === 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);
@@ -137,85 +52,14 @@ const ViewportWindowLevel = ({
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);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => {
setWindowLevels(data);
});
}, [viewportId, getWindowLevelsData]);
}, [viewportId, cornerstoneViewportService, getVolumeOpacity]);
const handleCornerstoneVOIModified = useCallback(
e => {
@@ -252,8 +96,8 @@ const ViewportWindowLevel = ({
[windowLevels]
);
const debouncedHandleCornerstoneVOIModified = useCallback(
debounce(handleCornerstoneVOIModified, 100),
const debouncedHandleCornerstoneVOIModified = useMemo(
() => debounce(handleCornerstoneVOIModified, 100),
[handleCornerstoneVOIModified]
);
@@ -280,7 +124,7 @@ const ViewportWindowLevel = ({
return;
}
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
@@ -291,49 +135,50 @@ const ViewportWindowLevel = ({
[getViewportsWithVolumeIds, cornerstoneViewportService]
);
// Listen to windowLevels changes and caches all the new ones
// New function to handle image volume loading completion
const handleImageVolumeLoadingCompleted = useCallback(() => {
setIsLoading(false);
updateViewportHistograms();
}, [updateViewportHistograms]);
// Listen to cornerstone events and set up interval for histogram updates
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);
eventTarget.addEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
handleImageVolumeLoadingCompleted
);
const intervalId = setInterval(() => {
if (isLoading) {
updateViewportHistograms();
}
}, 1000);
return () => {
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
updateViewportHistograms
);
document.removeEventListener(
Events.VOI_MODIFIED,
debouncedHandleCornerstoneVOIModified,
true
);
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
handleImageVolumeLoadingCompleted
);
clearInterval(intervalId);
};
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
}, [
updateViewportHistograms,
debouncedHandleCornerstoneVOIModified,
handleImageVolumeLoadingCompleted,
isLoading,
]);
// Create a memoized version of displaySet IDs for comparison
const displaySetIds = useMemo(() => {
return displaySets?.map(ds => ds.displaySetInstanceUID).sort() || [];
}, [displaySets]);
// 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,
@@ -344,10 +189,15 @@ const ViewportWindowLevel = ({
}
);
// Only update if displaySets actually changed and are loaded
if (displaySetIds.length && !isLoading) {
updateViewportHistograms();
}
return () => {
unsubscribe();
};
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return (
<PanelSection title="Window Level">
@@ -20,10 +20,6 @@ const workerFn = () => {
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) {
@@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
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 targetTimePoint = volume.numTimePoints - 1; // or any other time point you need
scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint);
} else {
scalarData = volume.voxelManager.getCompleteScalarDataArray();
}
if (!scalarData?.length) {
return undefined;
}
const { dimensions, origin, direction, spacing } = volume;
@@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
scalarData,
});
// after we calculate the range let's reset the timePointIndex
if (volume.numTimePoints > 1) {
volume.timePointIndex = prevTimePoint;
}
const { minimum: min, maximum: max } = range;
if (min === Infinity || max === -Infinity) {
return undefined;
}
const calcHistOptions = {
numBins: 256,
min: Math.max(min, options?.min ?? min),
@@ -0,0 +1,153 @@
import { cache as cs3DCache, Types } from '@cornerstonejs/core';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { utilities as csUtils } from '@cornerstonejs/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
/**
* Gets node opacity from volume actor
*/
export 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 follows a specific pattern
*/
export const isPetVolumeWithDefaultOpacity = (volumeId: string, volumeActor) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume || volume.metadata.Modality !== 'PT') {
return false;
}
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
if (volumeOpacity.getSize() < 2) {
return false;
}
const node1Value = [];
const node2Value = [];
volumeOpacity.getNodeValue(0, node1Value);
volumeOpacity.getNodeValue(1, node2Value);
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
return false;
}
const expectedOpacity = node2Value[1];
const opacitySize = volumeOpacity.getSize();
const currentNodeValue = [];
for (let i = 2; i < opacitySize; i++) {
volumeOpacity.getNodeValue(i, currentNodeValue);
if (currentNodeValue[1] !== expectedOpacity) {
return false;
}
}
return true;
};
/**
* Checks if volume has constant opacity
*/
export 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;
};
/**
* Gets window levels data for a viewport
*/
export const getWindowLevelsData = async (
viewport: Types.IStackViewport | Types.IVolumeViewport,
viewportInfo: any,
getVolumeOpacity: (viewport: any, volumeId: string) => number | undefined
) => {
if (!viewport) {
return [];
}
const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds();
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);
const opacity = getVolumeOpacity(viewport, volumeId);
const { metadata, scaling } = volume;
const modality = metadata.Modality;
const options = {
min: modality === 'PT' ? 0.1 : -999,
max: modality === 'PT' ? 5 : 10000,
};
const histogram = await getViewportVolumeHistogram(viewport, volume, options);
if (!histogram || histogram.range.min === histogram.range.max) {
return null;
}
if (!viewportInfo.displaySetOptions || !viewportInfo.displaySetOptions[volumeIndex]) {
return null;
}
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: viewportInfo.viewportId,
modality,
volumeId,
volumeIndex,
voi,
histogram,
colormap,
step: scaling?.PT ? 0.05 : 1,
opacity,
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
};
})
);
return windowLevels.filter(Boolean);
};
@@ -1,6 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
import { StackViewport } from '@cornerstonejs/core';
import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
export function Colormap({
@@ -54,8 +54,10 @@ export function Colormap({
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const { colormap } = viewport.getProperties(actorEntry.uid);
const actorEntry = actorEntries?.find(entry =>
entry.referencedId.includes(displaySetInstanceUID)
);
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
@@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
@@ -53,8 +53,6 @@ export function WindowLevelActionMenu({
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
const { t } = useTranslation('WindowLevelActionMenu');
const [viewportGrid] = useViewportGrid();
@@ -119,7 +117,7 @@ export function WindowLevelActionMenu({
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
)}
@@ -133,7 +131,7 @@ export function WindowLevelActionMenu({
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
colorbarProperties={colorbarProperties}
@@ -149,7 +147,7 @@ export function WindowLevelActionMenu({
<Colormap
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({
viewportId,
@@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({
commandsManager,
verticalDirection,
horizontalDirection,
}: withAppTypes): ReactNode {
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
displaySets: AppTypes.DisplaySet[];
}>): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
@@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const modalities = displaySets
.map(displaySet => displaySet.Modality)
.filter(modality => !nonWLModalities.includes(modality));
if (modalities.length === 0) {
return null;
}
return (
<WindowLevelActionMenu
viewportId={viewportId}
@@ -35,8 +35,8 @@ export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
export function ViewportActionCornersProvider({ children, service }) {
const viewportActionCornersReducer = (state, action) => {
switch (action.type) {
case 'SET_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
case 'ADD_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority } = action.payload;
// Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]]
@@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) {
// Insert the component from the payload but
// do not insert an undefined or null component.
if (component) {
const insertionIndex = locationComponents.findIndex(
component => indexPriority <= component.indexPriority
);
let insertionIndex;
const isRightSide =
location === ViewportActionCornersLocations.topRight ||
location === ViewportActionCornersLocations.bottomRight;
if (indexPriority === undefined) {
// If no indexPriority is provided, add it to the appropriate end
insertionIndex = isRightSide ? 0 : locationComponents.length;
} else {
if (isRightSide) {
insertionIndex = locationComponents.findIndex(
component => indexPriority > component.indexPriority
);
} else {
insertionIndex = locationComponents.findIndex(
component => indexPriority <= component.indexPriority
);
}
if (insertionIndex === -1) {
// If no suitable position found, add to the appropriate end
insertionIndex = isRightSide ? 0 : locationComponents.length;
}
}
const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
locationComponents = [
...locationComponents.slice(0, insertionIndex),
{
id,
component,
indexPriority,
indexPriority: indexPriority ?? defaultPriority,
},
...locationComponents.slice(insertionIndex + 1),
...locationComponents.slice(insertionIndex),
];
}
return {
...state,
...{
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
};
}
@@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) {
return viewportActionCornersState;
}, [viewportActionCornersState]);
const setComponent = useCallback(
const addComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => {
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
},
[dispatch]
);
const setComponents = useCallback(
const addComponents = useCallback(
(actionComponentInfos: Array<ActionComponentInfo>) => {
actionComponentInfos.forEach(actionComponentInfo =>
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
);
},
[dispatch]
@@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) {
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch]
);
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setComponent,
setComponents,
addComponent,
addComponents,
clear,
});
}
}, [getState, service, setComponent, setComponents]);
}, [getState, service, addComponent, addComponents, clear]);
// run many of the calls through the service itself since we want to publish events
const api = {
const viewportCornerActions = {
getState,
setComponent: props => service.setComponent(props),
setComponents: props => service.setComponents(props),
addComponent: props => service.addComponent(props),
addComponents: props => service.addComponents(props),
clear: props => service.clear(props),
};
const contextValue = useMemo(
() => [viewportActionCornersState, api],
[viewportActionCornersState, api]
() => [viewportActionCornersState, viewportCornerActions],
[viewportActionCornersState, viewportCornerActions]
);
return (
+9
View File
@@ -0,0 +1,9 @@
export const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
export const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const Enums = {
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION,
};
export default Enums;
@@ -1,6 +1,5 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
@@ -23,10 +22,12 @@ const tools = {
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
enabled: [
{ toolName: toolNames.SegmentationDisplay },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
@@ -38,13 +39,6 @@ const tools = {
function getCustomizationModule() {
return [
{
name: 'cornerstoneDicomUploadComponent',
value: {
id: 'dicomUploadComponent',
component: DicomUpload,
},
},
{
name: 'default',
value: [
@@ -120,7 +114,7 @@ function getCustomizationModule() {
type: 'value',
},
{
value: 'areaUnit',
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
@@ -140,7 +134,7 @@ function getCustomizationModule() {
},
{
displayName: 'Unit',
value: 'areaUnit',
value: 'areaUnits',
type: 'value',
},
],
@@ -170,12 +164,12 @@ function getCustomizationModule() {
type: 'value',
},
{
value: 'modalityUnit',
value: 'pixelValueUnits',
for: ['mean', 'max' /** 'stdDev **/],
type: 'unit',
},
{
value: 'areaUnit',
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
@@ -0,0 +1,111 @@
import React from 'react';
import { Toolbox } from '@ohif/ui-next';
import PanelSegmentation from './panels/PanelSegmentation';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
import PanelMeasurementTable from './panels/PanelMeasurement';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
const wrappedPanelSegmentation = ({ configuration }) => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
);
};
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
);
};
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
return (
<>
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
</>
);
};
const wrappedPanelMeasurement = ({ configuration }) => {
return (
<PanelMeasurementTable
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
);
};
return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
{
name: 'panelMeasurement',
iconName: 'tab-linear',
iconLabel: 'Measure',
label: 'Measurement',
component: wrappedPanelMeasurement,
},
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationNoHeader',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationNoHeader,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
},
];
};
export default getPanelModule;
@@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
const displaySet = {
plugin: 'microscopy',
Modality: 'SM',
viewportType: csEnums.ViewportType.WholeSlide,
viewportType: csEnums.ViewportType.WHOLE_SLIDE,
altImageText: 'Microscopy',
displaySetInstanceUID: utils.guid(),
SOPInstanceUID,
+55 -49
View File
@@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => {
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
const getDisabledState = (disabledText?: string) => ({
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
});
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const {
toolGroupService,
@@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return [
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
{
name: 'evaluate.viewport.supported',
evaluate: ({ viewportId, unsupportedViewportTypes, disabledText }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport && unsupportedViewportTypes?.includes(viewport.type)) {
return getDisabledState(disabledText);
}
return undefined;
},
},
{
name: 'evaluate.modality.supported',
evaluate: ({ viewportId, unsupportedModalities, supportedModalities, disabledText }) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
// Check for unsupported modalities (exclusion)
if (unsupportedModalities?.length) {
const hasUnsupportedModality = displaySets.some(displaySet =>
unsupportedModalities.includes(displaySet?.Modality)
);
if (hasUnsupportedModality) {
return getDisabledState(disabledText);
}
}
// Check for supported modalities (inclusion)
if (supportedModalities?.length) {
const hasAnySupportedModality = displaySets.some(displaySet =>
supportedModalities.includes(displaySet?.Modality)
);
if (!hasAnySupportedModality) {
return getDisabledState(disabledText || 'Tool not available for this modality');
}
}
},
},
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
return getDisabledState(disabledText);
}
const isPrimaryActive = toolNames
@@ -172,40 +220,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
};
},
},
{
name: 'evaluate.not3D',
evaluate: ({ viewportId, disabledText }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport?.type === 'volume3d') {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
},
},
{
name: 'evaluate.isUS',
evaluate: ({ viewportId, disabledText }) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
if (!isUS) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
},
},
{
name: 'evaluate.viewportProperties.toggle',
evaluate: ({ viewportId, button }) => {
@@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
});
if (!areReconstructable) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
return getDisabledState(disabledText);
}
const isMpr = protocol?.id === 'mpr';
@@ -288,11 +298,7 @@ function _evaluateToggle({
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',
};
return getDisabledState(disabledText);
}
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
@@ -0,0 +1,219 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { roundNumber } from '@ohif/core/src/utils';
import {
SegmentationData,
SegmentationRepresentation,
} from '../services/SegmentationService/SegmentationService';
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
// Get the readable text mapping once
const { readableText: readableTextMap } = customizationService.getCustomization(
'PanelSegmentation.readableText',
{}
);
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
const primary = [];
const indentation = ' '.repeat(indent);
for (const key in stats) {
if (Object.prototype.hasOwnProperty.call(stats, key)) {
const value = stats[key];
const readableText = readableTextMap?.[key];
if (!readableText) {
continue;
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Add empty row before category (except for the first category)
if (primary.length > 0) {
primary.push('');
}
// Add category title
primary.push(`${indentation}${readableText}`);
// Recursively handle nested objects
primary.push(...mapStatsToDisplay(value, indent + 1));
} else {
// For non-nested values, don't add empty rows
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
}
}
}
return primary;
}
// Get customization for display text mapping
const displayTextMapper = segment => {
const defaultDisplay = {
primary: [],
secondary: [],
};
// If the segment has cachedStats, map it to readable text
if (segment.cachedStats) {
const primary = mapStatsToDisplay(segment.cachedStats);
defaultDisplay.primary = primary;
}
return defaultDisplay;
};
const updatedSegments = {};
Object.entries(segments).forEach(([segmentIndex, segment]) => {
updatedSegments[segmentIndex] = {
...segment,
displayText: displayTextMapper(segment),
};
});
// Map the segments and apply the display text mapper
return {
...segmentation,
label,
segments: updatedSegments,
};
}
/**
* Represents the combination of segmentation data and its representation in a viewport.
*/
type ViewportSegmentationRepresentation = {
segmentationsWithRepresentations: {
representation: SegmentationRepresentation;
segmentation: SegmentationData;
}[];
disabled: boolean;
};
/**
* Custom hook that provides segmentation data and their representations for the active viewport.
* @param options - The options object.
* @param options.servicesManager - The services manager object.
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
* @param options.debounceTime - Debounce time in milliseconds for updates.
* @returns An array of segmentation data and their representations for the active viewport.
*/
export function useActiveViewportSegmentationRepresentations({
servicesManager,
subscribeToDataModified = false,
debounceTime = 0,
}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation {
const { segmentationService, viewportGridService, customizationService, displaySetService } =
servicesManager.services;
const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
useState<ViewportSegmentationRepresentation>({
segmentationsWithRepresentations: [],
disabled: false,
});
useEffect(() => {
const update = () => {
const viewportId = viewportGridService.getActiveViewportId();
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
if (!displaySet) {
return;
}
if (excludedModalities.includes(displaySet.Modality)) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: true,
}));
return;
}
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: false,
}));
return;
}
const representations = segmentationService.getSegmentationRepresentations(viewportId);
const newSegmentationsWithRepresentations = representations.map(representation => {
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
return {
representation,
segmentation: mappedSegmentation,
};
});
setSegmentationsWithRepresentations({
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
disabled: false,
});
};
const debouncedUpdate =
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
update();
const subscriptions = [
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REMOVED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
debouncedUpdate
),
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
debouncedUpdate
),
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
];
if (subscribeToDataModified) {
subscriptions.push(
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
debouncedUpdate
)
);
}
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
if (debounceTime > 0) {
debouncedUpdate.cancel();
}
};
}, [
segmentationService,
viewportGridService,
customizationService,
displaySetService,
debounceTime,
subscribeToDataModified,
]);
return segmentationsWithRepresentations;
}
@@ -0,0 +1,103 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
function mapMeasurementToDisplay(measurement, displaySetService) {
const { referenceSeriesUID } = measurement;
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
if (!displaySets[0]?.instances) {
throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
}
const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement;
const firstSite = findingSites?.[0];
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
// Initialize displayText with the structure used in Length.ts and CobbAngle.ts
const displayText = {
primary: [],
secondary: baseDisplayText?.secondary || [],
};
// Add baseDisplayText to primary if it exists
if (baseDisplayText) {
displayText.primary.push(...baseDisplayText.primary);
}
// Add finding sites to primary
if (findingSites) {
findingSites.forEach(site => {
if (site?.text && site.text !== label) {
displayText.primary.push(site.text);
}
});
}
// Add finding to primary if it's different from the label
if (finding && finding.text && finding.text !== label) {
displayText.primary.push(finding.text);
}
return {
...measurement,
displayText,
label,
};
}
/**
* A custom hook that provides mapped measurements based on the given services and filters.
*
* @param {Object} servicesManager - The services manager object.
* @param {Object} options - The options for filtering and mapping measurements.
* @param {Function} options.measurementFilter - Optional function to filter measurements.
* @param {Object} options.valueTypes - The value types for mapping measurements.
* @returns {Array} An array of mapped and filtered measurements.
*/
export function useMeasurements(servicesManager, { measurementFilter }) {
const { measurementService, displaySetService } = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => {
const updateDisplayMeasurements = () => {
let measurements = measurementService.getMeasurements();
if (measurementFilter) {
measurements = measurements.filter(measurementFilter);
}
const mappedMeasurements = measurements.map(m =>
mapMeasurementToDisplay(m, displaySetService)
);
setDisplayMeasurements(prevMeasurements => {
if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) {
return mappedMeasurements;
}
return prevMeasurements;
});
};
const debouncedUpdate = debounce(updateDisplayMeasurements, 100);
updateDisplayMeasurements();
const events = [
measurementService.EVENTS.MEASUREMENT_ADDED,
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
measurementService.EVENTS.MEASUREMENT_UPDATED,
measurementService.EVENTS.MEASUREMENT_REMOVED,
measurementService.EVENTS.MEASUREMENTS_CLEARED,
];
const subscriptions = events.map(
evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe
);
return () => {
subscriptions.forEach(unsub => unsub());
debouncedUpdate.cancel();
};
}, [measurementService, measurementFilter, displaySetService]);
return displayMeasurements;
}
Loaded 100 of 1273 files, more files were not shown because too many files have changed in this diff. Show more